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
@@ -3,6 +3,10 @@
3
3
  // src/cli.ts
4
4
  import { argv, stderr } from "node:process";
5
5
 
6
+ // src/proxy.ts
7
+ import { existsSync as existsSync11, realpathSync as realpathSync4 } from "node:fs";
8
+ import { basename as basename3, delimiter as delimiter4, dirname as dirname9, isAbsolute as isAbsolute4 } from "node:path";
9
+
6
10
  // ../mcp-stdio-core/src/record.ts
7
11
  function isPlainRecord(value) {
8
12
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -17,9 +21,6 @@ function errorResponse(id, code, message, data) {
17
21
  function jsonRpcId(value) {
18
22
  return typeof value === "string" || typeof value === "number" || value === null ? value : null;
19
23
  }
20
- function messageFromError(error) {
21
- return error instanceof Error ? error.message : String(error);
22
- }
23
24
  // ../mcp-stdio-core/src/transport.ts
24
25
  var HEADER_SEPARATOR = Buffer.from(`\r
25
26
  \r
@@ -206,37 +207,187 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
206
207
  closed: () => isClosed
207
208
  };
208
209
  }
209
- // ../lsp-core/src/tools/diagnostics.ts
210
- import { resolve as resolve4 } from "node:path";
211
-
212
210
  // ../lsp-core/src/lsp/client-wrapper.ts
213
- import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
214
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
211
+ import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
212
+ import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
215
213
 
216
214
  // ../lsp-core/src/request-context.ts
217
215
  import { AsyncLocalStorage } from "node:async_hooks";
216
+ import { existsSync, realpathSync, statSync } from "node:fs";
217
+ import { homedir } from "node:os";
218
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
219
+
220
+ class LspRequestContextParseError extends Error {
221
+ code;
222
+ name = "LspRequestContextParseError";
223
+ constructor(code, message) {
224
+ super(message);
225
+ this.code = code;
226
+ }
227
+ }
228
+
229
+ class LspRequestContextUnavailableError extends Error {
230
+ name = "LspRequestContextUnavailableError";
231
+ constructor() {
232
+ super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
233
+ }
234
+ }
218
235
  var storage = new AsyncLocalStorage;
236
+ var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
237
+ var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
219
238
  function runWithRequestContext(context, fn) {
220
239
  return storage.run(context, fn);
221
240
  }
241
+ function lspRequestContext() {
242
+ const context = storage.getStore();
243
+ if (!context)
244
+ throw new LspRequestContextUnavailableError;
245
+ return context;
246
+ }
222
247
  function contextCwd() {
223
- return storage.getStore()?.cwd ?? process.cwd();
248
+ return lspRequestContext().cwd;
249
+ }
250
+ function createStandaloneMcpRequestContext(input = {}) {
251
+ const env = input.env ?? process.env;
252
+ const cwd = canonicalCwd(input.cwd ?? process.cwd());
253
+ const home = input.homeDir ?? homedir();
254
+ const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
255
+ const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
256
+ const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
257
+ return parseLspRequestContext({
258
+ cwd,
259
+ projectConfigPaths,
260
+ userConfigPath,
261
+ installDecisionsPath,
262
+ capabilities: { installDecisionTool: true }
263
+ });
264
+ }
265
+ function parseLspRequestContext(value) {
266
+ if (!isRecord(value)) {
267
+ throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
268
+ }
269
+ rejectUnknownFields(value, CONTEXT_FIELDS, "context");
270
+ const cwd = stringField(value, "cwd");
271
+ const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
272
+ const userConfigPath = stringField(value, "userConfigPath");
273
+ const installDecisionsPath = stringField(value, "installDecisionsPath");
274
+ const capabilities = capabilitiesField(value["capabilities"]);
275
+ const canonical = canonicalCwd(cwd);
276
+ for (const path of projectConfigPaths) {
277
+ requireAbsolutePath(path, "projectConfigPaths");
278
+ const projectPath = canonicalizeExistingOrNearestAncestor(path);
279
+ if (!isPathInside(canonical, projectPath)) {
280
+ throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
281
+ }
282
+ }
283
+ requireAbsolutePath(userConfigPath, "userConfigPath");
284
+ requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
285
+ return {
286
+ cwd: canonical,
287
+ projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
288
+ userConfigPath,
289
+ installDecisionsPath,
290
+ capabilities
291
+ };
292
+ }
293
+ function translateProjectConfigEnv(value, cwd) {
294
+ if (value === undefined || value.length === 0)
295
+ return [join(cwd, ".codex", "lsp-client.json")];
296
+ return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
297
+ }
298
+ function translateHomeConfigEnv(value, home, fallback) {
299
+ if (value === undefined || value.length === 0)
300
+ return join(home, fallback);
301
+ return isAbsolute(value) ? value : join(home, value);
302
+ }
303
+ function canonicalCwd(cwd) {
304
+ const resolved = resolve(cwd);
305
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
306
+ throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
307
+ }
308
+ return realpathSync(resolved);
309
+ }
310
+ function canonicalizeExistingOrNearestAncestor(path) {
311
+ let current = resolve(path);
312
+ const suffix = [];
313
+ while (true) {
314
+ try {
315
+ const existing = realpathSync(current);
316
+ return suffix.length === 0 ? existing : join(existing, ...suffix);
317
+ } catch (error) {
318
+ if (!isMissingPathError(error))
319
+ throw error;
320
+ const parent = dirname(current);
321
+ if (parent === current)
322
+ throw error;
323
+ suffix.unshift(basename(current));
324
+ current = parent;
325
+ }
326
+ }
327
+ }
328
+ function capabilitiesField(value) {
329
+ if (!isRecord(value)) {
330
+ throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
331
+ }
332
+ rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
333
+ const installDecisionTool = value["installDecisionTool"];
334
+ if (typeof installDecisionTool !== "boolean") {
335
+ throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
336
+ }
337
+ return { installDecisionTool };
338
+ }
339
+ function stringField(value, field) {
340
+ const fieldValue = value[field];
341
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
342
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
343
+ }
344
+ return fieldValue;
345
+ }
346
+ function stringArrayField(value, field) {
347
+ const fieldValue = value[field];
348
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
349
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
350
+ }
351
+ return fieldValue;
352
+ }
353
+ function requireAbsolutePath(path, field) {
354
+ if (!isAbsolute(path)) {
355
+ throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
356
+ }
224
357
  }
225
- function contextEnv(key) {
226
- const store = storage.getStore();
227
- if (store?.env)
228
- return store.env[key];
229
- return process.env[key];
358
+ function isPathInside(parent, child) {
359
+ const childPath = resolve(child);
360
+ const relativePath = relative(parent, childPath);
361
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
362
+ }
363
+ function isMissingPathError(error) {
364
+ const code = errorCode(error);
365
+ return code === "ENOENT" || code === "ENOTDIR";
366
+ }
367
+ function rejectUnknownFields(value, allowed, scope) {
368
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
369
+ if (unknown.length > 0) {
370
+ throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
371
+ }
372
+ }
373
+ function isRecord(value) {
374
+ return typeof value === "object" && value !== null && !Array.isArray(value);
375
+ }
376
+ function errorCode(error) {
377
+ if (!error || typeof error !== "object" || !("code" in error))
378
+ return;
379
+ const code = Reflect.get(error, "code");
380
+ return typeof code === "string" ? code : undefined;
230
381
  }
231
382
 
232
383
  // ../lsp-core/src/lsp/effective-extension.ts
233
- import { basename, extname } from "node:path";
384
+ import { basename as basename2, extname } from "node:path";
234
385
  var BASENAME_EXTENSIONS = {
235
386
  Dockerfile: ".dockerfile",
236
387
  Containerfile: ".dockerfile"
237
388
  };
238
389
  function effectiveExtension(filePath) {
239
- return BASENAME_EXTENSIONS[basename(filePath)] ?? extname(filePath);
390
+ return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
240
391
  }
241
392
 
242
393
  // ../lsp-core/src/lsp/errors.ts
@@ -286,7 +437,12 @@ class LspInvalidPathError extends Error {
286
437
  }
287
438
 
288
439
  class LspServerLookupError extends Error {
440
+ lookup;
289
441
  name = "LspServerLookupError";
442
+ constructor(message, lookup) {
443
+ super(message);
444
+ this.lookup = lookup;
445
+ }
290
446
  }
291
447
 
292
448
  class LspServerInitializingError extends Error {
@@ -306,17 +462,18 @@ function isLspDeadConnectionError(err) {
306
462
  }
307
463
 
308
464
  // ../lsp-core/src/lsp/cleanup-errors.ts
309
- function reportBestEffortCleanupError(operation, error) {
310
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1")
311
- return;
465
+ function writeCleanupError(message) {
466
+ process.stderr.write(`${message}
467
+ `);
468
+ }
469
+ function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
312
470
  const message = error instanceof Error ? error.message : String(error);
313
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
471
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
314
472
  }
315
473
 
316
474
  // ../lsp-core/src/lsp/client.ts
317
- import { readFileSync } from "node:fs";
318
- import { resolve } from "node:path";
319
- import { pathToFileURL as pathToFileURL2 } from "node:url";
475
+ import { resolve as resolve6 } from "node:path";
476
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
320
477
 
321
478
  // ../lsp-core/src/lsp/connection.ts
322
479
  import { pathToFileURL } from "node:url";
@@ -380,28 +537,80 @@ class JsonRpcConnection {
380
537
  onError(handler) {
381
538
  this.errorHandlers.push(handler);
382
539
  }
383
- async sendRequest(method, params) {
540
+ async sendRequest(method, params, options = {}) {
384
541
  if (this.disposed)
385
542
  throw new Error("JSON-RPC connection is disposed");
386
543
  const id = this.nextRequestId;
387
544
  this.nextRequestId += 1;
545
+ const key = String(id);
388
546
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
389
- const responsePromise = new Promise((resolve, reject) => {
390
- this.pendingRequests.set(String(id), {
547
+ let requestWritten = false;
548
+ let cancelAfterWrite = false;
549
+ let settled = false;
550
+ const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
551
+ const responsePromise = new Promise((resolve2, reject) => {
552
+ const cleanup = () => {
553
+ options.signal?.removeEventListener("abort", onAbort);
554
+ };
555
+ const settleCancel = () => {
556
+ if (settled)
557
+ return;
558
+ settled = true;
559
+ this.pendingRequests.delete(key);
560
+ cleanup();
561
+ const rejectCancelled = () => reject(abortError(options.signal));
562
+ if (!requestWritten) {
563
+ cancelAfterWrite = true;
564
+ rejectCancelled();
565
+ return;
566
+ }
567
+ writeCancel().then(rejectCancelled, (error) => {
568
+ this.emitError(toError(error));
569
+ rejectCancelled();
570
+ });
571
+ };
572
+ const onAbort = () => settleCancel();
573
+ this.pendingRequests.set(key, {
391
574
  resolve(result) {
392
- resolve(result);
575
+ settled = true;
576
+ cleanup();
577
+ resolve2(result);
578
+ },
579
+ reject(error) {
580
+ settled = true;
581
+ cleanup();
582
+ reject(error);
393
583
  },
394
- reject
584
+ cleanup
395
585
  });
586
+ if (options.signal?.aborted) {
587
+ settleCancel();
588
+ return;
589
+ }
590
+ options.signal?.addEventListener("abort", onAbort, { once: true });
396
591
  });
592
+ if (settled)
593
+ return responsePromise;
397
594
  try {
398
595
  await this.writeMessage(message);
596
+ requestWritten = true;
597
+ if (cancelAfterWrite)
598
+ await writeCancel();
399
599
  } catch (error) {
400
- this.pendingRequests.delete(String(id));
600
+ if (settled)
601
+ return responsePromise;
602
+ const pending = this.pendingRequests.get(key);
603
+ if (pending) {
604
+ pending.cleanup();
605
+ this.pendingRequests.delete(key);
606
+ }
401
607
  throw error;
402
608
  }
403
609
  return responsePromise;
404
610
  }
611
+ pendingRequestCount() {
612
+ return this.pendingRequests.size;
613
+ }
405
614
  async sendNotification(method, params) {
406
615
  if (this.disposed)
407
616
  return;
@@ -418,6 +627,7 @@ class JsonRpcConnection {
418
627
  this.reader.off("error", this.handleStreamError);
419
628
  this.writer.off("error", this.handleStreamError);
420
629
  for (const pending of this.pendingRequests.values()) {
630
+ pending.cleanup();
421
631
  pending.reject(new Error("JSON-RPC connection disposed"));
422
632
  }
423
633
  this.pendingRequests.clear();
@@ -493,6 +703,7 @@ class JsonRpcConnection {
493
703
  if (!pending)
494
704
  return;
495
705
  this.pendingRequests.delete(String(id));
706
+ pending.cleanup();
496
707
  if ("error" in message) {
497
708
  pending.reject(jsonRpcErrorToError(message["error"]));
498
709
  return;
@@ -506,7 +717,11 @@ class JsonRpcConnection {
506
717
  try {
507
718
  handler(params);
508
719
  } catch (error) {
509
- this.emitError(toError(error));
720
+ if (error instanceof Error) {
721
+ this.emitError(error);
722
+ return;
723
+ }
724
+ this.emitError(new Error(String(error)));
510
725
  }
511
726
  }
512
727
  handleRequest(message) {
@@ -531,13 +746,13 @@ class JsonRpcConnection {
531
746
  const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
532
747
  \r
533
748
  ${body}`;
534
- return new Promise((resolve, reject) => {
749
+ return new Promise((resolve2, reject) => {
535
750
  this.writer.write(payload, (error) => {
536
751
  if (error) {
537
752
  reject(error);
538
753
  return;
539
754
  }
540
- resolve();
755
+ resolve2();
541
756
  });
542
757
  });
543
758
  }
@@ -547,6 +762,14 @@ ${body}`;
547
762
  }
548
763
  }
549
764
  }
765
+ function abortError(signal) {
766
+ const reason = signal?.reason;
767
+ if (reason instanceof Error)
768
+ return reason;
769
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
770
+ error.name = "AbortError";
771
+ return error;
772
+ }
550
773
  function parseContentLength2(headers) {
551
774
  for (const line of headers.split(`\r
552
775
  `)) {
@@ -586,8 +809,8 @@ function toError(error) {
586
809
 
587
810
  // ../lsp-core/src/lsp/process.ts
588
811
  import { spawn, spawnSync } from "node:child_process";
589
- import { existsSync, statSync } from "node:fs";
590
- import { delimiter, join } from "node:path";
812
+ import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
813
+ import { delimiter as delimiter2, join as join2 } from "node:path";
591
814
  function isMissingProcessError(error) {
592
815
  if (!(error instanceof Error) || !("code" in error))
593
816
  return false;
@@ -600,10 +823,10 @@ function reportKillError(context, error) {
600
823
  }
601
824
  function validateCwd(cwd) {
602
825
  try {
603
- if (!existsSync(cwd)) {
826
+ if (!existsSync2(cwd)) {
604
827
  return { valid: false, error: `Working directory does not exist: ${cwd}` };
605
828
  }
606
- const stats = statSync(cwd);
829
+ const stats = statSync2(cwd);
607
830
  if (!stats.isDirectory()) {
608
831
  return { valid: false, error: `Path is not a directory: ${cwd}` };
609
832
  }
@@ -616,9 +839,9 @@ function validateCwd(cwd) {
616
839
  }
617
840
  }
618
841
  function wrap(proc) {
619
- const exitedPromise = new Promise((resolve) => {
620
- proc.once("close", (code) => resolve(code ?? 0));
621
- proc.once("error", () => resolve(1));
842
+ const exitedPromise = new Promise((resolve2) => {
843
+ proc.once("close", (code) => resolve2(code ?? 0));
844
+ proc.once("error", () => resolve2(1));
622
845
  });
623
846
  if (!proc.stdin || !proc.stdout || !proc.stderr) {
624
847
  throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
@@ -672,7 +895,7 @@ function isWindowsShellShim(command) {
672
895
  return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
673
896
  }
674
897
  function splitPath(pathValue, platform) {
675
- const separator = platform === "win32" ? ";" : delimiter;
898
+ const separator = platform === "win32" ? ";" : delimiter2;
676
899
  return pathValue.split(separator).filter(Boolean);
677
900
  }
678
901
  function getWindowsPathExtensions(env) {
@@ -687,8 +910,8 @@ function resolveWindowsCommand(command, env) {
687
910
  const extensions = getWindowsPathExtensions(env);
688
911
  for (const baseDirectory of baseDirectories) {
689
912
  for (const extension of extensions) {
690
- const candidate = baseDirectory ? join(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
691
- if (existsSync(candidate))
913
+ const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
914
+ if (existsSync2(candidate))
692
915
  return candidate;
693
916
  }
694
917
  }
@@ -733,16 +956,16 @@ function spawnProcess(command, options) {
733
956
  return wrap(proc);
734
957
  }
735
958
 
736
- // ../lsp-core/src/lsp/transport.ts
737
- function isRecord(value) {
959
+ // ../lsp-core/src/lsp/transport-protocol.ts
960
+ function isRecord2(value) {
738
961
  return typeof value === "object" && value !== null && !Array.isArray(value);
739
962
  }
740
963
  function parseConfigurationItems(params) {
741
- if (!isRecord(params) || !Array.isArray(params["items"]))
964
+ if (!isRecord2(params) || !Array.isArray(params["items"]))
742
965
  return [];
743
966
  const items = [];
744
967
  for (const item of params["items"]) {
745
- if (!isRecord(item))
968
+ if (!isRecord2(item))
746
969
  continue;
747
970
  const section = item["section"];
748
971
  items.push(section === undefined || typeof section !== "string" ? {} : { section });
@@ -750,10 +973,35 @@ function parseConfigurationItems(params) {
750
973
  return items;
751
974
  }
752
975
  function parseDiagnosticsParams(params) {
753
- if (!isRecord(params) || typeof params["uri"] !== "string")
976
+ if (!isRecord2(params) || typeof params["uri"] !== "string")
754
977
  return null;
755
978
  const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
756
- return { uri: params["uri"], diagnostics };
979
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
980
+ return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
981
+ }
982
+ function createLspSpawnEnv(_root, input) {
983
+ return { ...input };
984
+ }
985
+ function isDiagnostic(value) {
986
+ return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
987
+ }
988
+ function isRange(value) {
989
+ return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
990
+ }
991
+ function isPosition(value) {
992
+ return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
993
+ }
994
+
995
+ // ../lsp-core/src/lsp/transport.ts
996
+ class LspClientNotStartedError extends Error {
997
+ serverId;
998
+ root;
999
+ name = "LspClientNotStartedError";
1000
+ constructor(serverId, root) {
1001
+ super("LSP client not started");
1002
+ this.serverId = serverId;
1003
+ this.root = root;
1004
+ }
757
1005
  }
758
1006
 
759
1007
  class LspClientTransport {
@@ -766,6 +1014,8 @@ class LspClientTransport {
766
1014
  diagnosticsStore = new Map;
767
1015
  requestTimeoutMs;
768
1016
  initializeTimeoutMs;
1017
+ workspaceApplyEditHandler = null;
1018
+ diagnosticPullSupported = false;
769
1019
  constructor(root, server2, timeouts = {}) {
770
1020
  this.root = root;
771
1021
  this.server = server2;
@@ -778,6 +1028,21 @@ class LspClientTransport {
778
1028
  command() {
779
1029
  return [...this.server.command];
780
1030
  }
1031
+ setWorkspaceApplyEditHandler(handler) {
1032
+ this.workspaceApplyEditHandler = handler;
1033
+ }
1034
+ hasWorkspaceApplyEditHandler() {
1035
+ return this.workspaceApplyEditHandler !== null;
1036
+ }
1037
+ setDiagnosticPullSupported(supported) {
1038
+ this.diagnosticPullSupported = supported;
1039
+ }
1040
+ isDiagnosticPullSupported() {
1041
+ return this.diagnosticPullSupported;
1042
+ }
1043
+ handlePublishDiagnostics(params) {
1044
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
1045
+ }
781
1046
  async start() {
782
1047
  const env = createLspSpawnEnv(this.root, {
783
1048
  ...process.env,
@@ -788,7 +1053,6 @@ class LspClientTransport {
788
1053
  env
789
1054
  });
790
1055
  this.startStderrReading();
791
- await new Promise((resolve) => setTimeout(resolve, 100));
792
1056
  if (this.proc.exitCode !== null) {
793
1057
  const stderr = this.stderrBuffer.join(`
794
1058
  `);
@@ -798,7 +1062,7 @@ class LspClientTransport {
798
1062
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
799
1063
  const diagnosticsParams = parseDiagnosticsParams(params);
800
1064
  if (diagnosticsParams?.uri) {
801
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
1065
+ this.handlePublishDiagnostics(diagnosticsParams);
802
1066
  }
803
1067
  });
804
1068
  this.connection.onRequest("workspace/configuration", (params) => {
@@ -811,6 +1075,9 @@ class LspClientTransport {
811
1075
  });
812
1076
  this.connection.onRequest("client/registerCapability", () => null);
813
1077
  this.connection.onRequest("window/workDoneProgress/create", () => null);
1078
+ if (this.workspaceApplyEditHandler) {
1079
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
1080
+ }
814
1081
  this.connection.onClose(() => {
815
1082
  this.processExited = true;
816
1083
  });
@@ -839,30 +1106,25 @@ class LspClientTransport {
839
1106
  }
840
1107
  async sendRequest(method, ...args) {
841
1108
  if (!this.connection)
842
- throw new Error("LSP client not started");
1109
+ throw new LspClientNotStartedError(this.server.id, this.root);
843
1110
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
844
1111
  const stderrTail = this.stderrBuffer.slice(-10).join(`
845
1112
  `);
846
1113
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
847
1114
  }
848
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
849
- let timeoutHandle = null;
850
- const timeoutPromise = new Promise((_, reject) => {
851
- timeoutHandle = setTimeout(() => {
852
- const stderrTail = this.stderrBuffer.slice(-5).join(`
1115
+ const options = args[1];
1116
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
1117
+ const timeoutController = new AbortController;
1118
+ const timeoutHandle = setTimeout(() => {
1119
+ const stderrTail = this.stderrBuffer.slice(-5).join(`
853
1120
  `);
854
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
855
- }, timeoutMs);
856
- });
1121
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
1122
+ }, timeoutMs);
1123
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
857
1124
  try {
858
- const requestPromise = args.length === 0 ? this.connection.sendRequest(method) : this.connection.sendRequest(method, args[0]);
859
- const result = await Promise.race([requestPromise, timeoutPromise]);
860
- if (timeoutHandle !== null)
861
- clearTimeout(timeoutHandle);
1125
+ const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
862
1126
  return result;
863
1127
  } catch (error) {
864
- if (timeoutHandle !== null)
865
- clearTimeout(timeoutHandle);
866
1128
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
867
1129
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
868
1130
  `) || undefined);
@@ -871,6 +1133,9 @@ class LspClientTransport {
871
1133
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
872
1134
  }
873
1135
  throw error;
1136
+ } finally {
1137
+ clearTimeout(timeoutHandle);
1138
+ combinedSignal.dispose();
874
1139
  }
875
1140
  }
876
1141
  async sendNotification(method, ...args) {
@@ -899,17 +1164,17 @@ class LspClientTransport {
899
1164
  try {
900
1165
  await this.sendRequest("shutdown");
901
1166
  } catch (error) {
902
- reportBestEffortCleanupError("shutdown request", error);
1167
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
903
1168
  }
904
1169
  try {
905
1170
  await this.sendNotification("exit");
906
1171
  } catch (error) {
907
- reportBestEffortCleanupError("exit notification", error);
1172
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
908
1173
  }
909
1174
  try {
910
1175
  this.connection.dispose();
911
1176
  } catch (error) {
912
- reportBestEffortCleanupError("connection dispose", error);
1177
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
913
1178
  }
914
1179
  this.connection = null;
915
1180
  }
@@ -920,8 +1185,8 @@ class LspClientTransport {
920
1185
  try {
921
1186
  proc.kill();
922
1187
  let timeoutId;
923
- const timeoutPromise = new Promise((resolve) => {
924
- timeoutId = setTimeout(resolve, STOP_HARD_KILL_TIMEOUT_MS);
1188
+ const timeoutPromise = new Promise((resolve2) => {
1189
+ timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
925
1190
  });
926
1191
  await Promise.race([
927
1192
  proc.exited.then(() => {
@@ -937,14 +1202,14 @@ class LspClientTransport {
937
1202
  proc.kill("SIGKILL");
938
1203
  await Promise.race([
939
1204
  proc.exited,
940
- new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
1205
+ new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
941
1206
  ]);
942
1207
  } catch (error) {
943
- reportBestEffortCleanupError("hard process kill", error);
1208
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
944
1209
  }
945
1210
  }
946
1211
  } catch (error) {
947
- reportBestEffortCleanupError("process stop", error);
1212
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
948
1213
  }
949
1214
  }
950
1215
  this.processExited = true;
@@ -954,26 +1219,45 @@ class LspClientTransport {
954
1219
  return this.diagnosticsStore.get(uri) ?? [];
955
1220
  }
956
1221
  }
957
- function createLspSpawnEnv(_root, input) {
958
- return { ...input };
959
- }
960
- function isDiagnostic(value) {
961
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
962
- }
963
- function isRange(value) {
964
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
965
- }
966
- function isPosition(value) {
967
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
1222
+ function combineAbortSignals(primary, secondary) {
1223
+ const controller = new AbortController;
1224
+ const abortFrom = (signal) => {
1225
+ if (!controller.signal.aborted)
1226
+ controller.abort(signal.reason);
1227
+ };
1228
+ const onPrimaryAbort = () => {
1229
+ if (primary)
1230
+ abortFrom(primary);
1231
+ };
1232
+ const onSecondaryAbort = () => abortFrom(secondary);
1233
+ if (primary?.aborted)
1234
+ abortFrom(primary);
1235
+ else
1236
+ primary?.addEventListener("abort", onPrimaryAbort, { once: true });
1237
+ if (secondary.aborted)
1238
+ abortFrom(secondary);
1239
+ else
1240
+ secondary.addEventListener("abort", onSecondaryAbort, { once: true });
1241
+ return {
1242
+ signal: controller.signal,
1243
+ dispose: () => {
1244
+ primary?.removeEventListener("abort", onPrimaryAbort);
1245
+ secondary.removeEventListener("abort", onSecondaryAbort);
1246
+ }
1247
+ };
968
1248
  }
969
1249
 
970
1250
  // ../lsp-core/src/lsp/connection.ts
971
- var INITIALIZE_SETTLE_MS = 300;
1251
+ function supportsDiagnosticPull(capabilities) {
1252
+ if (capabilities === undefined)
1253
+ return false;
1254
+ return Object.hasOwn(capabilities, "diagnosticProvider");
1255
+ }
972
1256
 
973
1257
  class LspClientConnection extends LspClientTransport {
974
1258
  async initialize() {
975
1259
  const rootUri = pathToFileURL(this.root).href;
976
- await this.sendRequest("initialize", {
1260
+ const result = await this.sendRequest("initialize", {
977
1261
  processId: process.pid,
978
1262
  rootUri,
979
1263
  rootPath: this.root,
@@ -987,8 +1271,7 @@ class LspClientConnection extends LspClientTransport {
987
1271
  publishDiagnostics: {},
988
1272
  rename: {
989
1273
  prepareSupport: true,
990
- prepareSupportDefaultBehavior: 1,
991
- honorsChangeAnnotations: true
1274
+ prepareSupportDefaultBehavior: 1
992
1275
  },
993
1276
  codeAction: {
994
1277
  codeActionLiteralSupport: {
@@ -1017,22 +1300,28 @@ class LspClientConnection extends LspClientTransport {
1017
1300
  symbol: {},
1018
1301
  workspaceFolders: true,
1019
1302
  configuration: true,
1020
- applyEdit: true,
1303
+ ...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
1021
1304
  workspaceEdit: {
1022
- documentChanges: true
1305
+ documentChanges: true,
1306
+ resourceOperations: ["create", "rename", "delete"]
1023
1307
  }
1024
1308
  }
1025
1309
  },
1026
1310
  initializationOptions: this.server.initialization
1027
1311
  }, { timeoutMs: this.initializeTimeoutMs });
1312
+ this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
1028
1313
  await this.sendNotification("initialized");
1029
1314
  await this.sendNotification("workspace/didChangeConfiguration", {
1030
1315
  settings: { json: { validate: { enable: true } } }
1031
1316
  });
1032
- await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
1033
1317
  }
1034
1318
  }
1035
1319
 
1320
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1321
+ import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
1322
+ import { relative as relative2, resolve as resolve2 } from "node:path";
1323
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
1324
+
1036
1325
  // ../lsp-core/src/lsp/language-mappings.ts
1037
1326
  var SYMBOL_KIND_MAP = {
1038
1327
  1: "File",
@@ -1205,164 +1494,1635 @@ function getLanguageId(ext) {
1205
1494
  return EXT_TO_LANG[ext] ?? "plaintext";
1206
1495
  }
1207
1496
 
1208
- // ../lsp-core/src/lsp/client.ts
1209
- var POST_OPEN_DELAY_MS = 1000;
1210
- var POST_DIAGNOSTICS_WAIT_MS = 500;
1497
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1498
+ var WATCHED_FILE_BATCH_SIZE = 128;
1499
+ var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1500
+ function canonicalPath(filePath) {
1501
+ const absolute = resolve2(filePath);
1502
+ try {
1503
+ return realpathSync2(absolute);
1504
+ } catch {
1505
+ return absolute;
1506
+ }
1507
+ }
1508
+ function isSameOrDescendant(candidate, parent) {
1509
+ const suffix = relative2(parent, candidate);
1510
+ return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
1511
+ }
1512
+ function movedPath(candidate, oldPath, newPath) {
1513
+ const suffix = relative2(oldPath, candidate);
1514
+ return suffix === "" ? newPath : resolve2(newPath, suffix);
1515
+ }
1211
1516
 
1212
- class LspClient extends LspClientConnection {
1213
- openedFiles = new Set;
1214
- documentVersions = new Map;
1215
- lastSyncedText = new Map;
1216
- diagnosticPullErrors = [];
1217
- getDiagnosticPullErrors() {
1218
- return this.diagnosticPullErrors;
1517
+ class WorkspaceDocumentState {
1518
+ sendNotification;
1519
+ clearDiagnostics;
1520
+ openDocuments = new Map;
1521
+ openByUri = new Map;
1522
+ openPromises = new Map;
1523
+ now;
1524
+ versionlessPublishQuiescenceMs;
1525
+ constructor(sendNotification, clearDiagnostics, options = {}) {
1526
+ this.sendNotification = sendNotification;
1527
+ this.clearDiagnostics = clearDiagnostics;
1528
+ this.now = options.now ?? (() => Date.now());
1529
+ this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1219
1530
  }
1220
1531
  async openFile(filePath) {
1221
- const absPath = resolve(contextCwd(), filePath);
1222
- const uri = pathToFileURL2(absPath).href;
1223
- const text = readFileSync(absPath, "utf-8");
1224
- if (!this.openedFiles.has(absPath)) {
1225
- const ext = effectiveExtension(absPath);
1226
- const languageId = getLanguageId(ext);
1227
- const version = 1;
1228
- await this.sendNotification("textDocument/didOpen", {
1229
- textDocument: {
1230
- uri,
1231
- languageId,
1232
- version,
1233
- text
1234
- }
1235
- });
1236
- this.openedFiles.add(absPath);
1237
- this.documentVersions.set(uri, version);
1238
- this.lastSyncedText.set(uri, text);
1239
- await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
1240
- return;
1532
+ const path = canonicalPath(filePath);
1533
+ const existingOpen = this.openPromises.get(path);
1534
+ if (existingOpen) {
1535
+ await existingOpen;
1536
+ return this.openFile(path);
1241
1537
  }
1242
- const prevText = this.lastSyncedText.get(uri);
1243
- if (prevText === text) {
1538
+ const text = readFileSync(path, "utf-8");
1539
+ const existing = this.openDocuments.get(path);
1540
+ if (!existing)
1541
+ return this.openDocumentSingleFlight(path, text);
1542
+ if (existing.text === text)
1244
1543
  return;
1245
- }
1246
- const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
1247
- this.documentVersions.set(uri, nextVersion);
1248
- this.lastSyncedText.set(uri, text);
1249
- await this.sendNotification("textDocument/didChange", {
1250
- textDocument: { uri, version: nextVersion },
1251
- contentChanges: [{ text }]
1252
- });
1253
- await this.sendNotification("textDocument/didSave", {
1254
- textDocument: { uri },
1255
- text
1256
- });
1544
+ await this.changeDocument(existing, text);
1257
1545
  }
1258
- async definition(filePath, line, character) {
1259
- const absPath = resolve(contextCwd(), filePath);
1260
- await this.openFile(absPath);
1261
- return this.sendRequest("textDocument/definition", {
1262
- textDocument: { uri: pathToFileURL2(absPath).href },
1263
- position: { line: line - 1, character }
1264
- });
1546
+ getVersion(filePath) {
1547
+ return this.openDocuments.get(canonicalPath(filePath))?.version;
1265
1548
  }
1266
- async references(filePath, line, character, includeDeclaration = true) {
1267
- const absPath = resolve(contextCwd(), filePath);
1268
- await this.openFile(absPath);
1269
- return this.sendRequest("textDocument/references", {
1270
- textDocument: { uri: pathToFileURL2(absPath).href },
1271
- position: { line: line - 1, character },
1272
- context: { includeDeclaration }
1273
- });
1549
+ getStoredDiagnostics(uri) {
1550
+ const state = this.openByUri.get(uri);
1551
+ if (!state)
1552
+ return [];
1553
+ return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
1554
+ }
1555
+ captureDiagnosticSnapshot(filePath) {
1556
+ const state = this.openDocuments.get(canonicalPath(filePath));
1557
+ if (!state)
1558
+ return null;
1559
+ return {
1560
+ path: state.path,
1561
+ uri: state.uri,
1562
+ version: state.version,
1563
+ documentGeneration: state.generation,
1564
+ publishGeneration: state.publishGeneration
1565
+ };
1274
1566
  }
1275
- async documentSymbols(filePath) {
1276
- const absPath = resolve(contextCwd(), filePath);
1277
- await this.openFile(absPath);
1278
- return this.sendRequest("textDocument/documentSymbol", {
1279
- textDocument: { uri: pathToFileURL2(absPath).href }
1280
- });
1567
+ isCurrentSnapshot(snapshot) {
1568
+ const state = this.openDocuments.get(snapshot.path);
1569
+ return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1281
1570
  }
1282
- async workspaceSymbols(query) {
1283
- return this.sendRequest("workspace/symbol", { query });
1571
+ getPullCache(snapshot) {
1572
+ const state = this.openByUri.get(snapshot.uri);
1573
+ if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1574
+ return null;
1575
+ return state.pullCache;
1284
1576
  }
1285
- isUnsupportedDiagnosticPullError(error) {
1286
- if (!(error instanceof Error))
1287
- return false;
1288
- const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
1289
- if (code === -32601)
1290
- return true;
1291
- return /unsupported|not supported|method not found|unknown request/i.test(error.message);
1577
+ recordPullDiagnostics(snapshot, report) {
1578
+ const state = this.openByUri.get(snapshot.uri);
1579
+ if (!state)
1580
+ return;
1581
+ state.pullCache = {
1582
+ documentVersion: snapshot.version,
1583
+ diagnostics: [...report.diagnostics],
1584
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
1585
+ };
1292
1586
  }
1293
- async diagnostics(filePath) {
1294
- const absPath = resolve(contextCwd(), filePath);
1295
- const uri = pathToFileURL2(absPath).href;
1296
- await this.openFile(absPath);
1297
- await new Promise((r) => setTimeout(r, POST_DIAGNOSTICS_WAIT_MS));
1298
- try {
1299
- const result = await this.sendRequest("textDocument/diagnostic", {
1300
- textDocument: { uri }
1301
- });
1302
- if (result.items) {
1303
- return { items: result.items };
1587
+ recordPublishedDiagnostics(params) {
1588
+ const state = this.openByUri.get(params.uri);
1589
+ if (!state)
1590
+ return;
1591
+ state.publishGeneration += 1;
1592
+ state.lastPublish = {
1593
+ diagnostics: [...params.diagnostics],
1594
+ publishGeneration: state.publishGeneration,
1595
+ documentGenerationAtArrival: state.generation,
1596
+ arrivedAt: this.now(),
1597
+ ...params.version === undefined ? {} : { version: params.version }
1598
+ };
1599
+ this.notifyWaiters(state);
1600
+ }
1601
+ resolvePushDiagnostics(snapshot) {
1602
+ const state = this.openByUri.get(snapshot.uri);
1603
+ if (!state?.lastPublish)
1604
+ return { status: "missing" };
1605
+ const publish = state.lastPublish;
1606
+ if (publish.version !== undefined) {
1607
+ return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
1608
+ }
1609
+ if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
1610
+ return { status: "missing" };
1611
+ const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
1612
+ const waitMs = Math.max(0, readyAt - this.now());
1613
+ return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1614
+ }
1615
+ waitForDiagnosticsActivity(snapshot, timeoutMs) {
1616
+ const state = this.openByUri.get(snapshot.uri);
1617
+ if (!state || timeoutMs <= 0)
1618
+ return Promise.resolve();
1619
+ return new Promise((resolveActivity) => {
1620
+ let settled = false;
1621
+ const finish = () => {
1622
+ if (settled)
1623
+ return;
1624
+ settled = true;
1625
+ clearTimeout(timer);
1626
+ state.waiters.delete(finish);
1627
+ resolveActivity();
1628
+ };
1629
+ const timer = setTimeout(finish, timeoutMs);
1630
+ if (typeof timer.unref === "function")
1631
+ timer.unref();
1632
+ state.waiters.add(finish);
1633
+ });
1634
+ }
1635
+ validateVersions(operations) {
1636
+ const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
1637
+ for (const operation of operations) {
1638
+ if (operation.kind === "text") {
1639
+ const current = versions.get(operation.path);
1640
+ if (operation.documentVersion !== null && current !== operation.documentVersion) {
1641
+ const observed = current === undefined ? "closed document" : `open document version ${current}`;
1642
+ return {
1643
+ changeIndex: operation.changeIndex,
1644
+ message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
1645
+ };
1646
+ }
1647
+ if (current !== undefined)
1648
+ versions.set(operation.path, current + 1);
1649
+ continue;
1304
1650
  }
1305
- } catch (error) {
1306
- if (!this.isUnsupportedDiagnosticPullError(error)) {
1307
- this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
1651
+ if (operation.kind === "rename") {
1652
+ const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
1653
+ for (const [path] of moved)
1654
+ versions.delete(path);
1655
+ for (const [path] of moved)
1656
+ versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
1657
+ continue;
1658
+ }
1659
+ if (operation.kind === "delete") {
1660
+ for (const path of [...versions.keys()]) {
1661
+ if (isSameOrDescendant(path, operation.path))
1662
+ versions.delete(path);
1663
+ }
1664
+ continue;
1665
+ }
1666
+ if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
1667
+ versions.set(operation.path, 1);
1308
1668
  }
1309
1669
  }
1310
- return { items: this.getStoredDiagnostics(uri) };
1670
+ return null;
1311
1671
  }
1312
- async prepareRename(filePath, line, character) {
1313
- const absPath = resolve(contextCwd(), filePath);
1314
- await this.openFile(absPath);
1315
- return this.sendRequest("textDocument/prepareRename", {
1316
- textDocument: { uri: pathToFileURL2(absPath).href },
1317
- position: { line: line - 1, character }
1318
- });
1672
+ async synchronize(delta) {
1673
+ const watched = [];
1674
+ for (const mutation of delta.operations)
1675
+ await this.synchronizeMutation(mutation, watched);
1676
+ for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
1677
+ await this.sendNotification("workspace/didChangeWatchedFiles", {
1678
+ changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
1679
+ });
1680
+ }
1319
1681
  }
1320
- async rename(filePath, line, character, newName) {
1321
- const absPath = resolve(contextCwd(), filePath);
1322
- await this.openFile(absPath);
1323
- return this.sendRequest("textDocument/rename", {
1324
- textDocument: { uri: pathToFileURL2(absPath).href },
1325
- position: { line: line - 1, character },
1326
- newName
1682
+ async synchronizeMutation(mutation, watched) {
1683
+ if (mutation.kind === "text") {
1684
+ const state = this.openDocuments.get(mutation.path);
1685
+ if (state)
1686
+ await this.changeDocument(state, mutation.afterText);
1687
+ else
1688
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
1689
+ return;
1690
+ }
1691
+ if (mutation.kind === "create") {
1692
+ const state = this.openDocuments.get(mutation.path);
1693
+ if (state) {
1694
+ await this.closeDocument(state);
1695
+ await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
1696
+ } else {
1697
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
1698
+ }
1699
+ return;
1700
+ }
1701
+ if (mutation.kind === "rename") {
1702
+ const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
1703
+ for (const state of moved)
1704
+ await this.closeDocument(state);
1705
+ for (const state of moved) {
1706
+ const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
1707
+ await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
1708
+ }
1709
+ if (moved.length === 0) {
1710
+ watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
1711
+ watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
1712
+ }
1713
+ return;
1714
+ }
1715
+ const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
1716
+ for (const state of removed)
1717
+ await this.closeDocument(state);
1718
+ if (removed.length === 0)
1719
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
1720
+ }
1721
+ async openDocumentSingleFlight(path, text) {
1722
+ const existing = this.openPromises.get(path);
1723
+ if (existing)
1724
+ return existing;
1725
+ const open = (async () => {
1726
+ const state = {
1727
+ path,
1728
+ uri: pathToFileURL2(path).href,
1729
+ languageId: getLanguageId(effectiveExtension(path)),
1730
+ text,
1731
+ version: 1,
1732
+ generation: 1,
1733
+ publishGeneration: 0,
1734
+ waiters: new Set
1735
+ };
1736
+ this.openDocuments.set(path, state);
1737
+ this.openByUri.set(state.uri, state);
1738
+ this.notifyWaiters(state);
1739
+ await this.sendNotification("textDocument/didOpen", {
1740
+ textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
1741
+ });
1742
+ })().finally(() => {
1743
+ this.openPromises.delete(path);
1744
+ });
1745
+ this.openPromises.set(path, open);
1746
+ return open;
1747
+ }
1748
+ async changeDocument(state, text) {
1749
+ state.text = text;
1750
+ state.version += 1;
1751
+ state.generation += 1;
1752
+ this.clearDiagnostics(state.uri);
1753
+ this.notifyWaiters(state);
1754
+ await this.sendNotification("textDocument/didChange", {
1755
+ textDocument: { uri: state.uri, version: state.version },
1756
+ contentChanges: [{ text }]
1327
1757
  });
1758
+ await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
1759
+ }
1760
+ async closeDocument(state) {
1761
+ this.openDocuments.delete(state.path);
1762
+ this.openByUri.delete(state.uri);
1763
+ this.clearDiagnostics(state.uri);
1764
+ this.notifyWaiters(state);
1765
+ await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
1766
+ }
1767
+ notifyWaiters(state) {
1768
+ for (const waiter of [...state.waiters])
1769
+ waiter();
1328
1770
  }
1329
1771
  }
1330
1772
 
1331
- // ../lsp-core/src/lsp/process-signal-cleanup.ts
1332
- function installProcessSignalCleanup(cleanup) {
1333
- const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
1334
- const handler = () => {
1335
- cleanup().catch((error) => {
1336
- reportBestEffortCleanupError("signal cleanup", error);
1337
- });
1338
- };
1339
- for (const signal of signals) {
1340
- process.on(signal, handler);
1341
- }
1342
- return () => {
1343
- for (const signal of signals) {
1344
- process.removeListener(signal, handler);
1345
- }
1346
- };
1773
+ // ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
1774
+ var CONCURRENT_FAILURE_REASON_BY_PHASE = {
1775
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
1776
+ settled: "workspace/applyEdit was already handled for this workspace mutation"
1777
+ };
1778
+ function workspaceApplyEditConcurrentFailureReason(phase) {
1779
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
1347
1780
  }
1348
1781
 
1349
- // ../lsp-core/src/lsp/manager.ts
1350
- async function stopClientBestEffort(client) {
1782
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1783
+ import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
1784
+
1785
+ // ../lsp-core/src/lsp/workspace-edit-path.ts
1786
+ import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
1787
+ import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
1788
+ import { fileURLToPath } from "node:url";
1789
+
1790
+ class WorkspaceEditPathError extends Error {
1791
+ path;
1792
+ detail;
1793
+ name = "WorkspaceEditPathError";
1794
+ constructor(path, detail) {
1795
+ super(`${detail}: ${path}`);
1796
+ this.path = path;
1797
+ this.detail = detail;
1798
+ }
1799
+ }
1800
+ function isPathInsideWorkspace(filePath, workspaceRoot) {
1801
+ const relativePath = relative3(workspaceRoot, filePath);
1802
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
1803
+ }
1804
+ function canonicalizeMissingPath(filePath) {
1805
+ let ancestor = filePath;
1806
+ while (!existsSync3(ancestor)) {
1807
+ const parent = dirname2(ancestor);
1808
+ if (parent === ancestor)
1809
+ throw new WorkspaceEditPathError(filePath, "no existing ancestor");
1810
+ ancestor = parent;
1811
+ }
1812
+ return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
1813
+ }
1814
+ function canonicalWorkspaceRoot(workspaceRoot) {
1351
1815
  try {
1352
- await client.stop();
1816
+ const canonical = realpathSync3(resolve3(workspaceRoot));
1817
+ if (!lstatSync(canonical).isDirectory()) {
1818
+ return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
1819
+ }
1820
+ return {
1821
+ success: true,
1822
+ path: canonical,
1823
+ requestedPath: resolve3(workspaceRoot),
1824
+ followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
1825
+ };
1353
1826
  } catch (error) {
1354
- reportBestEffortCleanupError("client stop", error);
1827
+ const detail = error instanceof Error ? error.message : String(error);
1828
+ return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
1355
1829
  }
1356
1830
  }
1357
- function awaitWithSignal(promise, signal) {
1358
- if (!signal)
1359
- return promise;
1360
- return new Promise((resolve2, reject) => {
1361
- let settled = false;
1362
- const onAbort = () => {
1363
- if (settled)
1364
- return;
1365
- settled = true;
1831
+ function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
1832
+ let requestedPath;
1833
+ try {
1834
+ const parsed = new URL(uri);
1835
+ if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
1836
+ return { success: false, error: `non-file URI ${uri}` };
1837
+ }
1838
+ requestedPath = resolve3(fileURLToPath(parsed));
1839
+ } catch (error) {
1840
+ const detail = error instanceof Error ? error.message : String(error);
1841
+ return { success: false, error: `non-file URI ${uri}: ${detail}` };
1842
+ }
1843
+ try {
1844
+ const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
1845
+ if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
1846
+ return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
1847
+ }
1848
+ return {
1849
+ success: true,
1850
+ path: canonical,
1851
+ requestedPath,
1852
+ followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
1853
+ };
1854
+ } catch (error) {
1855
+ const detail = error instanceof Error ? error.message : String(error);
1856
+ return { success: false, error: `${requestedPath}: ${detail}` };
1857
+ }
1858
+ }
1859
+ function snapshotPath(path, includeChildren) {
1860
+ if (!existsSync3(path))
1861
+ return { kind: "missing" };
1862
+ const stats = lstatSync(path);
1863
+ if (stats.isFile())
1864
+ return { kind: "file", content: readFileSync2(path, "utf-8") };
1865
+ if (stats.isDirectory()) {
1866
+ return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
1867
+ }
1868
+ throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
1869
+ }
1870
+
1871
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1872
+ var DEFAULT_IO = {
1873
+ writeFile(path, content) {
1874
+ writeFileSync(path, content, "utf-8");
1875
+ },
1876
+ rename(oldPath, newPath) {
1877
+ renameSync(oldPath, newPath);
1878
+ },
1879
+ remove(path, recursive) {
1880
+ rmSync(path, { recursive, force: false });
1881
+ }
1882
+ };
1883
+ function snapshotsEqual(expected, actual) {
1884
+ if (expected.kind !== actual.kind)
1885
+ return false;
1886
+ if (expected.kind === "file" && actual.kind === "file")
1887
+ return expected.content === actual.content;
1888
+ if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
1889
+ return JSON.stringify(expected.children) === JSON.stringify(actual.children);
1890
+ }
1891
+ return true;
1892
+ }
1893
+ function liveSnapshot(path, expected) {
1894
+ return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
1895
+ }
1896
+ function firstOperationIndex(plan) {
1897
+ return plan.operations[0]?.changeIndex ?? 0;
1898
+ }
1899
+ function failedCommit(plan, failure) {
1900
+ const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
1901
+ return {
1902
+ result: {
1903
+ success: false,
1904
+ filesModified,
1905
+ totalEdits,
1906
+ errors: [`change ${changeIndex}: ${message}`],
1907
+ failedChange: changeIndex,
1908
+ ...lateAbort ? { lateAbort: true } : {}
1909
+ },
1910
+ delta: mutationDelta(mutations),
1911
+ fingerprint: plan.fingerprint
1912
+ };
1913
+ }
1914
+ function verifySnapshots(plan) {
1915
+ for (const [path, expected] of plan.snapshots) {
1916
+ let actual;
1917
+ try {
1918
+ actual = liveSnapshot(path, expected);
1919
+ } catch (error) {
1920
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1921
+ const detail = error instanceof Error ? error.message : String(error);
1922
+ return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
1923
+ }
1924
+ if (!snapshotsEqual(expected, actual)) {
1925
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1926
+ return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
1927
+ }
1928
+ }
1929
+ return null;
1930
+ }
1931
+ function addModifiedPath(paths, path) {
1932
+ if (!paths.includes(path))
1933
+ paths.push(path);
1934
+ }
1935
+ function reportedPath(plan, path) {
1936
+ return plan.reportedPathByCanonical.get(path) ?? path;
1937
+ }
1938
+ function changedPathsForMutation(mutation) {
1939
+ return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
1940
+ }
1941
+ function mutationDelta(operations) {
1942
+ const changedPaths = new Set;
1943
+ for (const operation of operations) {
1944
+ for (const path of changedPathsForMutation(operation))
1945
+ changedPaths.add(path);
1946
+ }
1947
+ return { operations, changedPaths: [...changedPaths].sort() };
1948
+ }
1949
+ function resolveIo(overrides) {
1950
+ return {
1951
+ writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
1952
+ rename: overrides?.rename ?? DEFAULT_IO.rename,
1953
+ remove: overrides?.remove ?? DEFAULT_IO.remove
1954
+ };
1955
+ }
1956
+ function commitOperation(context, operation) {
1957
+ const { plan, io, accumulator } = context;
1958
+ if (operation.kind === "noop")
1959
+ return;
1960
+ if (operation.kind === "text") {
1961
+ io.writeFile(operation.path, operation.afterText);
1962
+ accumulator.mutations.push({
1963
+ kind: "text",
1964
+ path: operation.path,
1965
+ beforeText: operation.beforeText,
1966
+ afterText: operation.afterText
1967
+ });
1968
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1969
+ accumulator.totalEdits += operation.editCount;
1970
+ return;
1971
+ }
1972
+ if (operation.kind === "create") {
1973
+ io.writeFile(operation.path, "");
1974
+ accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
1975
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1976
+ return;
1977
+ }
1978
+ if (operation.kind === "rename") {
1979
+ if (operation.replaceDestination) {
1980
+ const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
1981
+ io.remove(operation.newPath, targetKind === "directory");
1982
+ accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
1983
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1984
+ }
1985
+ io.rename(operation.oldPath, operation.newPath);
1986
+ accumulator.mutations.push({
1987
+ kind: "rename",
1988
+ oldPath: operation.oldPath,
1989
+ newPath: operation.newPath,
1990
+ sourceKind: operation.sourceKind
1991
+ });
1992
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1993
+ return;
1994
+ }
1995
+ io.remove(operation.path, operation.recursive);
1996
+ accumulator.mutations.push({
1997
+ kind: "delete",
1998
+ path: operation.path,
1999
+ targetKind: operation.targetKind
2000
+ });
2001
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
2002
+ }
2003
+ function commitWorkspaceEditPlan(plan, options = {}) {
2004
+ if (options.signal?.aborted) {
2005
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2006
+ }
2007
+ const stale = verifySnapshots(plan);
2008
+ if (stale)
2009
+ return stale;
2010
+ if (options.signal?.aborted) {
2011
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2012
+ }
2013
+ const io = resolveIo(options.io);
2014
+ const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
2015
+ const context = { plan, io, accumulator };
2016
+ let lateAbort = false;
2017
+ for (const operation of plan.operations) {
2018
+ try {
2019
+ commitOperation(context, operation);
2020
+ } catch (error) {
2021
+ const detail = error instanceof Error ? error.message : String(error);
2022
+ return failedCommit(plan, {
2023
+ message: `I/O failure during ${operation.kind}: ${detail}`,
2024
+ changeIndex: operation.changeIndex,
2025
+ mutations: accumulator.mutations,
2026
+ filesModified: accumulator.filesModified,
2027
+ totalEdits: accumulator.totalEdits,
2028
+ lateAbort: lateAbort || options.signal?.aborted === true
2029
+ });
2030
+ }
2031
+ if (options.signal?.aborted)
2032
+ lateAbort = true;
2033
+ }
2034
+ const result = {
2035
+ success: true,
2036
+ filesModified: accumulator.filesModified,
2037
+ totalEdits: accumulator.totalEdits,
2038
+ errors: [],
2039
+ ...lateAbort ? { lateAbort: true } : {}
2040
+ };
2041
+ return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
2042
+ }
2043
+
2044
+ // ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
2045
+ import { createHash } from "node:crypto";
2046
+ function canonicalFingerprint(operations) {
2047
+ const canonical = operations.map((operation) => {
2048
+ switch (operation.kind) {
2049
+ case "text":
2050
+ return {
2051
+ kind: operation.kind,
2052
+ changeIndex: operation.changeIndex,
2053
+ path: operation.path,
2054
+ edits: operation.edits,
2055
+ version: operation.version
2056
+ };
2057
+ case "rename":
2058
+ return {
2059
+ kind: operation.kind,
2060
+ changeIndex: operation.changeIndex,
2061
+ oldPath: operation.oldPath,
2062
+ newPath: operation.newPath,
2063
+ overwrite: operation.overwrite,
2064
+ ignoreIfExists: operation.ignoreIfExists
2065
+ };
2066
+ case "create":
2067
+ return {
2068
+ kind: operation.kind,
2069
+ changeIndex: operation.changeIndex,
2070
+ path: operation.path,
2071
+ overwrite: operation.overwrite,
2072
+ ignoreIfExists: operation.ignoreIfExists
2073
+ };
2074
+ case "delete":
2075
+ return {
2076
+ kind: operation.kind,
2077
+ changeIndex: operation.changeIndex,
2078
+ path: operation.path,
2079
+ recursive: operation.recursive,
2080
+ ignoreIfNotExists: operation.ignoreIfNotExists
2081
+ };
2082
+ }
2083
+ });
2084
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
2085
+ }
2086
+
2087
+ // ../lsp-core/src/lsp/workspace-edit-types.ts
2088
+ class WorkspaceEditValidationError extends Error {
2089
+ changeIndex;
2090
+ detail;
2091
+ name = "WorkspaceEditValidationError";
2092
+ constructor(changeIndex, detail) {
2093
+ super(`change ${changeIndex}: ${detail}`);
2094
+ this.changeIndex = changeIndex;
2095
+ this.detail = detail;
2096
+ }
2097
+ }
2098
+
2099
+ // ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
2100
+ function isRecord3(value) {
2101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2102
+ }
2103
+ function parsePosition(value) {
2104
+ if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
2105
+ return null;
2106
+ }
2107
+ return { line: value["line"], character: value["character"] };
2108
+ }
2109
+ function parseRange(value) {
2110
+ if (!isRecord3(value))
2111
+ return null;
2112
+ const start = parsePosition(value["start"]);
2113
+ const end = parsePosition(value["end"]);
2114
+ return start && end ? { start, end } : null;
2115
+ }
2116
+ function parseTextEdits(value, changeIndex) {
2117
+ if (!Array.isArray(value)) {
2118
+ throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
2119
+ }
2120
+ const edits = [];
2121
+ for (const candidate of value) {
2122
+ if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
2123
+ throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
2124
+ }
2125
+ if ("annotationId" in candidate) {
2126
+ throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
2127
+ }
2128
+ const range = parseRange(candidate["range"]);
2129
+ if (!range)
2130
+ throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
2131
+ edits.push({ range, newText: candidate["newText"] });
2132
+ }
2133
+ return edits;
2134
+ }
2135
+ function parseBooleanOption(options, key, changeIndex) {
2136
+ const value = options[key];
2137
+ if (value === undefined)
2138
+ return false;
2139
+ if (typeof value !== "boolean") {
2140
+ throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
2141
+ }
2142
+ return value;
2143
+ }
2144
+ function parseOptions(value, allowed, changeIndex) {
2145
+ if (value === undefined)
2146
+ return {};
2147
+ if (!isRecord3(value))
2148
+ throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
2149
+ for (const key of Object.keys(value)) {
2150
+ if (!allowed.includes(key))
2151
+ throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
2152
+ }
2153
+ const parsed = {};
2154
+ for (const key of allowed)
2155
+ parsed[key] = parseBooleanOption(value, key, changeIndex);
2156
+ return parsed;
2157
+ }
2158
+
2159
+ // ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
2160
+ function parseResourceChange(input) {
2161
+ const kind = input.change["kind"];
2162
+ if (kind === "create" || kind === "delete") {
2163
+ parseSinglePathResource(input, kind);
2164
+ return;
2165
+ }
2166
+ if (kind !== "rename") {
2167
+ throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
2168
+ }
2169
+ parseRename(input);
2170
+ }
2171
+ function parseSinglePathResource(input, kind) {
2172
+ const { change, changeIndex, workspaceRoot, target } = input;
2173
+ if (typeof change["uri"] !== "string")
2174
+ throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
2175
+ const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
2176
+ if (!resolvedPath.success) {
2177
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2178
+ return;
2179
+ }
2180
+ if (kind === "create") {
2181
+ const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2182
+ target.operations.push({
2183
+ kind,
2184
+ changeIndex,
2185
+ path: resolvedPath.path,
2186
+ reportedPath: resolvedPath.requestedPath,
2187
+ overwrite: options2["overwrite"] ?? false,
2188
+ ignoreIfExists: options2["ignoreIfExists"] ?? false,
2189
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2190
+ });
2191
+ return;
2192
+ }
2193
+ const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
2194
+ target.operations.push({
2195
+ kind,
2196
+ changeIndex,
2197
+ path: resolvedPath.path,
2198
+ reportedPath: resolvedPath.requestedPath,
2199
+ recursive: options["recursive"] ?? false,
2200
+ ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
2201
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2202
+ });
2203
+ }
2204
+ function parseRename(input) {
2205
+ const { change, changeIndex, workspaceRoot, target } = input;
2206
+ if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
2207
+ throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
2208
+ }
2209
+ const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
2210
+ const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
2211
+ if (!oldPath.success || !newPath.success) {
2212
+ target.failures.push({
2213
+ changeIndex,
2214
+ message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
2215
+ });
2216
+ return;
2217
+ }
2218
+ const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2219
+ target.operations.push({
2220
+ kind: "rename",
2221
+ changeIndex,
2222
+ oldPath: oldPath.path,
2223
+ newPath: newPath.path,
2224
+ reportedOldPath: oldPath.requestedPath,
2225
+ reportedNewPath: newPath.requestedPath,
2226
+ overwrite: options["overwrite"] ?? false,
2227
+ ignoreIfExists: options["ignoreIfExists"] ?? false,
2228
+ followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
2229
+ });
2230
+ }
2231
+
2232
+ // ../lsp-core/src/lsp/workspace-edit-parser.ts
2233
+ function failureResult(failures) {
2234
+ const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
2235
+ const first = sorted[0];
2236
+ return {
2237
+ success: false,
2238
+ filesModified: [],
2239
+ totalEdits: 0,
2240
+ errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
2241
+ ...first ? { failedChange: first.changeIndex } : {}
2242
+ };
2243
+ }
2244
+ function parseWorkspaceEdit(edit, workspaceRoot) {
2245
+ if (!isRecord3(edit))
2246
+ return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
2247
+ if (edit["changeAnnotations"] !== undefined) {
2248
+ return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
2249
+ }
2250
+ const hasChanges = edit["changes"] !== undefined;
2251
+ const hasDocumentChanges = edit["documentChanges"] !== undefined;
2252
+ if (hasChanges && hasDocumentChanges) {
2253
+ return {
2254
+ operations: [],
2255
+ failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
2256
+ };
2257
+ }
2258
+ const target = { operations: [], failures: [] };
2259
+ if (hasChanges)
2260
+ return parseChanges(edit["changes"], workspaceRoot, target);
2261
+ return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
2262
+ }
2263
+ function parseChanges(value, workspaceRoot, target) {
2264
+ if (!isRecord3(value))
2265
+ return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
2266
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
2267
+ for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
2268
+ const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
2269
+ if (!resolvedPath.success) {
2270
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2271
+ continue;
2272
+ }
2273
+ try {
2274
+ target.operations.push({
2275
+ kind: "text",
2276
+ changeIndex,
2277
+ path: resolvedPath.path,
2278
+ reportedPath: resolvedPath.requestedPath,
2279
+ edits: parseTextEdits(rawEdits, changeIndex),
2280
+ version: null
2281
+ });
2282
+ } catch (error) {
2283
+ if (error instanceof WorkspaceEditValidationError) {
2284
+ target.failures.push({ changeIndex, message: error.detail });
2285
+ continue;
2286
+ }
2287
+ throw error;
2288
+ }
2289
+ }
2290
+ return target;
2291
+ }
2292
+ function parseDocumentChanges(value, workspaceRoot, target) {
2293
+ if (value === undefined)
2294
+ return target;
2295
+ if (!Array.isArray(value)) {
2296
+ return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
2297
+ }
2298
+ for (const [changeIndex, change] of value.entries()) {
2299
+ try {
2300
+ parseDocumentChange({ change, changeIndex, workspaceRoot, target });
2301
+ } catch (error) {
2302
+ if (error instanceof WorkspaceEditValidationError) {
2303
+ target.failures.push({ changeIndex, message: error.detail });
2304
+ continue;
2305
+ }
2306
+ throw error;
2307
+ }
2308
+ }
2309
+ return target;
2310
+ }
2311
+ function parseDocumentChange(input) {
2312
+ const { change, changeIndex, workspaceRoot, target } = input;
2313
+ if (!isRecord3(change))
2314
+ throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
2315
+ if ("annotationId" in change) {
2316
+ throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
2317
+ }
2318
+ if (typeof change["kind"] === "string") {
2319
+ parseResourceChange({ change, changeIndex, workspaceRoot, target });
2320
+ return;
2321
+ }
2322
+ const identifier = change["textDocument"];
2323
+ if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
2324
+ throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
2325
+ }
2326
+ const version = identifier["version"];
2327
+ if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
2328
+ throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
2329
+ }
2330
+ const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
2331
+ if (!resolvedPath.success) {
2332
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2333
+ return;
2334
+ }
2335
+ target.operations.push({
2336
+ kind: "text",
2337
+ changeIndex,
2338
+ path: resolvedPath.path,
2339
+ reportedPath: resolvedPath.requestedPath,
2340
+ edits: parseTextEdits(change["edits"], changeIndex),
2341
+ version
2342
+ });
2343
+ }
2344
+
2345
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2346
+ import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
2347
+
2348
+ // ../lsp-core/src/lsp/workspace-edit-text.ts
2349
+ function comparePosition(left, right) {
2350
+ return left.line === right.line ? left.character - right.character : left.line - right.line;
2351
+ }
2352
+ function positionsEqual(left, right) {
2353
+ return left.line === right.line && left.character === right.character;
2354
+ }
2355
+ function rangesEqual(left, right) {
2356
+ return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
2357
+ }
2358
+ function isEmptyRange(range) {
2359
+ return positionsEqual(range.start, range.end);
2360
+ }
2361
+ function formatRange(range) {
2362
+ return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
2363
+ }
2364
+ function validatePosition(position, label, context) {
2365
+ const { lines, changeIndex } = context;
2366
+ if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
2367
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
2368
+ }
2369
+ if (position.line < 0 || position.character < 0) {
2370
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
2371
+ }
2372
+ const line = lines[position.line];
2373
+ if (line === undefined) {
2374
+ throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
2375
+ }
2376
+ if (position.character > line.length) {
2377
+ throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
2378
+ }
2379
+ }
2380
+ function validateRange(range, lines, changeIndex) {
2381
+ const context = { lines, changeIndex };
2382
+ validatePosition(range.start, "start", context);
2383
+ validatePosition(range.end, "end", context);
2384
+ if (comparePosition(range.start, range.end) > 0) {
2385
+ throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
2386
+ }
2387
+ }
2388
+ function sortAndDeduplicate(edits) {
2389
+ const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
2390
+ const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
2391
+ return positionOrder === 0 ? right.index - left.index : positionOrder;
2392
+ });
2393
+ const unique = [];
2394
+ for (const entry of sorted) {
2395
+ const previous = unique.at(-1);
2396
+ if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
2397
+ continue;
2398
+ }
2399
+ unique.push(entry.edit);
2400
+ }
2401
+ return unique;
2402
+ }
2403
+ function validateNoOverlap(edits, changeIndex) {
2404
+ for (let index = 0;index < edits.length - 1; index += 1) {
2405
+ const later = edits[index];
2406
+ const earlier = edits[index + 1];
2407
+ if (later === undefined || earlier === undefined)
2408
+ continue;
2409
+ if (comparePosition(earlier.range.end, later.range.start) > 0) {
2410
+ throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
2411
+ }
2412
+ }
2413
+ }
2414
+ function applyNormalizedTextEdits(content, edits) {
2415
+ const lines = content.split(`
2416
+ `);
2417
+ for (const edit of edits) {
2418
+ const { start, end } = edit.range;
2419
+ const startLine = lines[start.line];
2420
+ const endLine = lines[end.line];
2421
+ if (startLine === undefined || endLine === undefined)
2422
+ continue;
2423
+ const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
2424
+ lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
2425
+ `));
2426
+ }
2427
+ return lines.join(`
2428
+ `);
2429
+ }
2430
+ function normalizeTextEdits(content, edits, changeIndex) {
2431
+ const lines = content.split(`
2432
+ `);
2433
+ for (const edit of edits) {
2434
+ validateRange(edit.range, lines, changeIndex);
2435
+ }
2436
+ const normalized = sortAndDeduplicate(edits);
2437
+ validateNoOverlap(normalized, changeIndex);
2438
+ return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
2439
+ }
2440
+
2441
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2442
+ function isSameOrDescendant2(candidate, parent) {
2443
+ const relativePath = relative4(parent, candidate);
2444
+ return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
2445
+ }
2446
+ function removeVirtualSubtree(virtual, path) {
2447
+ for (const candidate of [...virtual.keys()]) {
2448
+ if (isSameOrDescendant2(candidate, path))
2449
+ virtual.delete(candidate);
2450
+ }
2451
+ virtual.set(path, { kind: "missing" });
2452
+ }
2453
+ function moveVirtualSubtree(virtual, oldPath, newPath) {
2454
+ const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
2455
+ removeVirtualSubtree(virtual, oldPath);
2456
+ removeVirtualSubtree(virtual, newPath);
2457
+ for (const [candidate, entry] of moved) {
2458
+ const suffix = relative4(oldPath, candidate);
2459
+ virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
2460
+ }
2461
+ }
2462
+ function virtualDirectoryHasChildren(virtual, path) {
2463
+ for (const [candidate, entry] of virtual) {
2464
+ if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
2465
+ return true;
2466
+ }
2467
+ return false;
2468
+ }
2469
+ function requireVirtualParent(virtual, path, changeIndex) {
2470
+ if (virtual.get(dirname3(path))?.kind !== "directory") {
2471
+ throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
2472
+ }
2473
+ }
2474
+ function simulateOperations(parsed, snapshots) {
2475
+ const virtual = new Map(snapshots);
2476
+ const planned = [];
2477
+ const failures = [];
2478
+ for (const operation of parsed) {
2479
+ try {
2480
+ planned.push(simulateOperation(operation, virtual));
2481
+ } catch (error) {
2482
+ if (error instanceof WorkspaceEditValidationError) {
2483
+ failures.push({ changeIndex: operation.changeIndex, message: error.detail });
2484
+ continue;
2485
+ }
2486
+ throw error;
2487
+ }
2488
+ }
2489
+ return { operations: planned, failures };
2490
+ }
2491
+ function simulateOperation(operation, virtual) {
2492
+ switch (operation.kind) {
2493
+ case "text":
2494
+ return simulateText(operation, virtual);
2495
+ case "create":
2496
+ return simulateCreate(operation, virtual);
2497
+ case "rename":
2498
+ return simulateRename(operation, virtual);
2499
+ case "delete":
2500
+ return simulateDelete(operation, virtual);
2501
+ }
2502
+ }
2503
+ function rejectSymbolicLink(operation) {
2504
+ if (operation.followedSymbolicLink) {
2505
+ throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
2506
+ }
2507
+ }
2508
+ function simulateText(operation, virtual) {
2509
+ const entry = virtual.get(operation.path);
2510
+ if (entry?.kind !== "file")
2511
+ throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
2512
+ const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
2513
+ virtual.set(operation.path, { kind: "file", content: normalized.text });
2514
+ return {
2515
+ kind: "text",
2516
+ changeIndex: operation.changeIndex,
2517
+ path: operation.path,
2518
+ beforeText: entry.content,
2519
+ afterText: normalized.text,
2520
+ editCount: normalized.edits.length,
2521
+ documentVersion: operation.version
2522
+ };
2523
+ }
2524
+ function simulateCreate(operation, virtual) {
2525
+ rejectSymbolicLink(operation);
2526
+ requireVirtualParent(virtual, operation.path, operation.changeIndex);
2527
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2528
+ if (target.kind !== "missing") {
2529
+ if (operation.overwrite && target.kind === "file") {
2530
+ virtual.set(operation.path, { kind: "file", content: "" });
2531
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
2532
+ }
2533
+ if (operation.ignoreIfExists)
2534
+ return { kind: "noop", changeIndex: operation.changeIndex };
2535
+ throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
2536
+ }
2537
+ virtual.set(operation.path, { kind: "file", content: "" });
2538
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
2539
+ }
2540
+ function simulateRename(operation, virtual) {
2541
+ rejectSymbolicLink(operation);
2542
+ const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
2543
+ if (source.kind === "missing") {
2544
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
2545
+ }
2546
+ if (operation.oldPath === operation.newPath)
2547
+ return { kind: "noop", changeIndex: operation.changeIndex };
2548
+ if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
2549
+ throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
2550
+ }
2551
+ requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
2552
+ const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
2553
+ if (destination.kind !== "missing" && !operation.overwrite) {
2554
+ if (operation.ignoreIfExists)
2555
+ return { kind: "noop", changeIndex: operation.changeIndex };
2556
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
2557
+ }
2558
+ moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
2559
+ return {
2560
+ kind: "rename",
2561
+ changeIndex: operation.changeIndex,
2562
+ oldPath: operation.oldPath,
2563
+ newPath: operation.newPath,
2564
+ sourceKind: source.kind,
2565
+ replaceDestination: destination.kind !== "missing"
2566
+ };
2567
+ }
2568
+ function simulateDelete(operation, virtual) {
2569
+ rejectSymbolicLink(operation);
2570
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2571
+ if (target.kind === "missing") {
2572
+ if (operation.ignoreIfNotExists)
2573
+ return { kind: "noop", changeIndex: operation.changeIndex };
2574
+ throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
2575
+ }
2576
+ if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
2577
+ throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
2578
+ }
2579
+ removeVirtualSubtree(virtual, operation.path);
2580
+ return {
2581
+ kind: "delete",
2582
+ changeIndex: operation.changeIndex,
2583
+ path: operation.path,
2584
+ targetKind: target.kind,
2585
+ recursive: operation.recursive
2586
+ };
2587
+ }
2588
+
2589
+ // ../lsp-core/src/lsp/workspace-edit-snapshot.ts
2590
+ import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
2591
+ import { dirname as dirname4, resolve as resolve5 } from "node:path";
2592
+ class WorkspaceSnapshotBuilder {
2593
+ workspaceRoot;
2594
+ snapshots = new Map;
2595
+ constructor(workspaceRoot) {
2596
+ this.workspaceRoot = workspaceRoot;
2597
+ }
2598
+ build(operations) {
2599
+ this.add(this.workspaceRoot, false);
2600
+ for (const operation of operations) {
2601
+ switch (operation.kind) {
2602
+ case "rename":
2603
+ this.add(operation.oldPath, true);
2604
+ this.add(operation.newPath, true);
2605
+ break;
2606
+ case "delete":
2607
+ this.add(operation.path, true);
2608
+ break;
2609
+ case "text":
2610
+ case "create":
2611
+ this.add(operation.path, false);
2612
+ break;
2613
+ }
2614
+ }
2615
+ return this.snapshots;
2616
+ }
2617
+ add(path, includeChildren) {
2618
+ let candidate = path;
2619
+ while (true) {
2620
+ const existing = this.snapshots.get(candidate);
2621
+ if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
2622
+ this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
2623
+ }
2624
+ if (candidate === this.workspaceRoot)
2625
+ break;
2626
+ candidate = dirname4(candidate);
2627
+ }
2628
+ if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
2629
+ return;
2630
+ for (const child of readdirSync2(path))
2631
+ this.add(resolve5(path, child), true);
2632
+ }
2633
+ }
2634
+ function snapshotOperations(operations, workspaceRoot) {
2635
+ return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
2636
+ }
2637
+
2638
+ // ../lsp-core/src/lsp/workspace-edit-plan.ts
2639
+ class PlanPathIndex {
2640
+ firstChangeByPath = new Map;
2641
+ reportedPathByCanonical = new Map;
2642
+ build(operations) {
2643
+ for (const operation of operations) {
2644
+ switch (operation.kind) {
2645
+ case "rename":
2646
+ this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
2647
+ this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
2648
+ break;
2649
+ case "text":
2650
+ case "create":
2651
+ case "delete":
2652
+ this.add(operation.path, operation.reportedPath, operation.changeIndex);
2653
+ break;
2654
+ }
2655
+ }
2656
+ }
2657
+ add(path, reportedPath2, changeIndex) {
2658
+ if (!this.firstChangeByPath.has(path))
2659
+ this.firstChangeByPath.set(path, changeIndex);
2660
+ if (!this.reportedPathByCanonical.has(path))
2661
+ this.reportedPathByCanonical.set(path, reportedPath2);
2662
+ }
2663
+ }
2664
+ function fingerprintWorkspaceEdit(edit, workspaceRoot) {
2665
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2666
+ if (!root.success)
2667
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2668
+ const parsed = parseWorkspaceEdit(edit, root.path);
2669
+ if (parsed.failures.length > 0)
2670
+ return { success: false, result: failureResult(parsed.failures) };
2671
+ return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
2672
+ }
2673
+ function planWorkspaceEdit(edit, workspaceRoot) {
2674
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2675
+ if (!root.success)
2676
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2677
+ const parsed = parseWorkspaceEdit(edit, root.path);
2678
+ if (parsed.failures.length > 0)
2679
+ return { success: false, result: failureResult(parsed.failures) };
2680
+ let snapshots;
2681
+ try {
2682
+ snapshots = snapshotOperations(parsed.operations, root.path);
2683
+ } catch (error) {
2684
+ return {
2685
+ success: false,
2686
+ result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
2687
+ };
2688
+ }
2689
+ const simulated = simulateOperations(parsed.operations, snapshots);
2690
+ if (simulated.failures.length > 0)
2691
+ return { success: false, result: failureResult(simulated.failures) };
2692
+ const paths = new PlanPathIndex;
2693
+ paths.build(parsed.operations);
2694
+ const plan = {
2695
+ workspaceRoot: root.path,
2696
+ operations: simulated.operations,
2697
+ snapshots,
2698
+ firstChangeByPath: paths.firstChangeByPath,
2699
+ reportedPathByCanonical: paths.reportedPathByCanonical,
2700
+ fingerprint: canonicalFingerprint(parsed.operations)
2701
+ };
2702
+ return { success: true, plan };
2703
+ }
2704
+
2705
+ // ../lsp-core/src/lsp/workspace-mutation-controller.ts
2706
+ function failure(message, failedChange, base) {
2707
+ return {
2708
+ success: false,
2709
+ filesModified: base?.filesModified ?? [],
2710
+ totalEdits: base?.totalEdits ?? 0,
2711
+ errors: [message],
2712
+ ...failedChange === undefined ? {} : { failedChange },
2713
+ ...base?.lateAbort ? { lateAbort: true } : {}
2714
+ };
2715
+ }
2716
+ function responseFor(result) {
2717
+ if (result.success)
2718
+ return { applied: true };
2719
+ return {
2720
+ applied: false,
2721
+ failureReason: result.errors[0] ?? "workspace edit failed",
2722
+ ...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
2723
+ };
2724
+ }
2725
+ function isRecord4(value) {
2726
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2727
+ }
2728
+
2729
+ class WorkspaceMutationController {
2730
+ workspaceRoot;
2731
+ documents;
2732
+ activeLease = null;
2733
+ nextLeaseId = 1;
2734
+ io;
2735
+ constructor(workspaceRoot, documents) {
2736
+ this.workspaceRoot = workspaceRoot;
2737
+ this.documents = documents;
2738
+ }
2739
+ setIo(io) {
2740
+ this.io = io;
2741
+ }
2742
+ acquire(signal) {
2743
+ if (this.activeLease)
2744
+ return { success: false, result: failure("workspace mutation is already in progress") };
2745
+ if (signal?.aborted)
2746
+ return { success: false, result: failure("cancelled before mutating request") };
2747
+ const lease = {
2748
+ id: this.nextLeaseId,
2749
+ phase: "idle",
2750
+ ...signal === undefined ? {} : { signal }
2751
+ };
2752
+ this.nextLeaseId += 1;
2753
+ this.activeLease = lease;
2754
+ return { success: true, lease };
2755
+ }
2756
+ release(lease) {
2757
+ if (this.activeLease?.id !== lease.id)
2758
+ return;
2759
+ this.activeLease.phase = "sealed";
2760
+ this.activeLease = null;
2761
+ }
2762
+ isBeforeCommit(lease) {
2763
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
2764
+ }
2765
+ async handleApplyEdit(params) {
2766
+ const lease = this.activeLease;
2767
+ if (!lease)
2768
+ return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
2769
+ if (lease.phase !== "idle") {
2770
+ return {
2771
+ applied: false,
2772
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
2773
+ };
2774
+ }
2775
+ lease.phase = "applying";
2776
+ lease.applyCompletion = new Promise((resolve6) => {
2777
+ lease.resolveApply = resolve6;
2778
+ });
2779
+ const edit = isRecord4(params) ? params["edit"] : undefined;
2780
+ const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
2781
+ lease.serverApply = record2;
2782
+ lease.phase = "settled";
2783
+ lease.resolveApply?.();
2784
+ return responseFor(record2.result);
2785
+ }
2786
+ async reconcileRename(leaseToken, edit) {
2787
+ const lease = this.requireActiveLease(leaseToken);
2788
+ if (!lease)
2789
+ return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
2790
+ if (lease.phase === "applying")
2791
+ await lease.applyCompletion;
2792
+ if (lease.serverApply)
2793
+ return this.reconcileServerApply(lease.serverApply, edit);
2794
+ lease.phase = "sealed";
2795
+ if (!edit)
2796
+ return { edit, apply: failure("No edit provided") };
2797
+ const applied = await this.applyEdit(edit, lease);
2798
+ return { edit, apply: applied.result };
2799
+ }
2800
+ reconcileServerApply(record2, edit) {
2801
+ if (!edit)
2802
+ return { edit, apply: record2.result };
2803
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
2804
+ if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
2805
+ return { edit, apply: record2.result };
2806
+ }
2807
+ return {
2808
+ edit,
2809
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
2810
+ };
2811
+ }
2812
+ async applyEdit(edit, lease) {
2813
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
2814
+ if (!planned.success)
2815
+ return { fingerprint: null, result: planned.result };
2816
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
2817
+ if (versionFailure) {
2818
+ return {
2819
+ fingerprint: planned.plan.fingerprint,
2820
+ result: failure(versionFailure.message, versionFailure.changeIndex)
2821
+ };
2822
+ }
2823
+ const commit = commitWorkspaceEditPlan(planned.plan, {
2824
+ ...lease.signal === undefined ? {} : { signal: lease.signal },
2825
+ ...this.io === undefined ? {} : { io: this.io }
2826
+ });
2827
+ let result = commit.result;
2828
+ if (commit.delta.operations.length > 0) {
2829
+ try {
2830
+ await this.documents.synchronize(commit.delta);
2831
+ } catch (error) {
2832
+ const message = error instanceof Error ? error.message : String(error);
2833
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
2834
+ }
2835
+ }
2836
+ if (lease.signal?.aborted && !result.lateAbort)
2837
+ result = { ...result, lateAbort: true };
2838
+ return { fingerprint: planned.plan.fingerprint, result };
2839
+ }
2840
+ requireActiveLease(lease) {
2841
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
2842
+ }
2843
+ }
2844
+
2845
+ // ../lsp-core/src/lsp/client.ts
2846
+ var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
2847
+ var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
2848
+
2849
+ class LspClient extends LspClientConnection {
2850
+ diagnosticPullErrors = [];
2851
+ documents;
2852
+ workspaceMutations;
2853
+ diagnosticsFreshnessTimeoutMs;
2854
+ constructor(root, server2, options = {}) {
2855
+ super(root, server2, options);
2856
+ this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2857
+ this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2858
+ versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2859
+ });
2860
+ this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
2861
+ this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
2862
+ }
2863
+ getDiagnosticPullErrors() {
2864
+ return this.diagnosticPullErrors;
2865
+ }
2866
+ async openFile(filePath) {
2867
+ const absPath = this.resolveWorkspacePath(filePath);
2868
+ await this.documents.openFile(absPath);
2869
+ }
2870
+ getOpenDocumentVersion(filePath) {
2871
+ return this.documents.getVersion(this.resolveWorkspacePath(filePath));
2872
+ }
2873
+ getStoredDiagnostics(uri) {
2874
+ return [...this.documents.getStoredDiagnostics(uri)];
2875
+ }
2876
+ setWorkspaceEditIo(io) {
2877
+ this.workspaceMutations.setIo(io);
2878
+ }
2879
+ handlePublishDiagnostics(params) {
2880
+ super.handlePublishDiagnostics(params);
2881
+ this.documents.recordPublishedDiagnostics(params);
2882
+ }
2883
+ async definition(filePath, line, character, signal) {
2884
+ const absPath = this.resolveWorkspacePath(filePath);
2885
+ await this.openFile(absPath);
2886
+ const options = signal === undefined ? {} : { signal };
2887
+ return this.sendRequest("textDocument/definition", {
2888
+ textDocument: { uri: pathToFileURL3(absPath).href },
2889
+ position: { line: line - 1, character }
2890
+ }, options);
2891
+ }
2892
+ async references(filePath, line, character, includeDeclaration = true, signal) {
2893
+ const absPath = this.resolveWorkspacePath(filePath);
2894
+ await this.openFile(absPath);
2895
+ const options = signal === undefined ? {} : { signal };
2896
+ return this.sendRequest("textDocument/references", {
2897
+ textDocument: { uri: pathToFileURL3(absPath).href },
2898
+ position: { line: line - 1, character },
2899
+ context: { includeDeclaration }
2900
+ }, options);
2901
+ }
2902
+ async documentSymbols(filePath, signal) {
2903
+ const absPath = this.resolveWorkspacePath(filePath);
2904
+ await this.openFile(absPath);
2905
+ const options = signal === undefined ? {} : { signal };
2906
+ return this.sendRequest("textDocument/documentSymbol", {
2907
+ textDocument: { uri: pathToFileURL3(absPath).href }
2908
+ }, options);
2909
+ }
2910
+ async workspaceSymbols(query, signal) {
2911
+ const options = signal === undefined ? {} : { signal };
2912
+ return this.sendRequest("workspace/symbol", { query }, options);
2913
+ }
2914
+ isUnsupportedDiagnosticPullError(error) {
2915
+ if (!(error instanceof Error))
2916
+ return false;
2917
+ const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
2918
+ if (code === -32601)
2919
+ return true;
2920
+ return /unsupported|not supported|method not found|unknown request/i.test(error.message);
2921
+ }
2922
+ freshnessTimeout(absPath) {
2923
+ return {
2924
+ items: [],
2925
+ transientError: {
2926
+ kind: "freshness_timeout",
2927
+ message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
2928
+ }
2929
+ };
2930
+ }
2931
+ parseDiagnosticPullReport(value) {
2932
+ if (value.kind === "unchanged") {
2933
+ return {
2934
+ type: "unchanged",
2935
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2936
+ };
2937
+ }
2938
+ return {
2939
+ type: "full",
2940
+ diagnostics: value.items ?? [],
2941
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2942
+ };
2943
+ }
2944
+ async diagnostics(filePath, signal) {
2945
+ signal?.throwIfAborted();
2946
+ const absPath = this.resolveWorkspacePath(filePath);
2947
+ const uri = pathToFileURL3(absPath).href;
2948
+ await this.openFile(absPath);
2949
+ const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
2950
+ for (;; ) {
2951
+ signal?.throwIfAborted();
2952
+ const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
2953
+ if (!snapshot)
2954
+ return this.freshnessTimeout(absPath);
2955
+ const push = this.documents.resolvePushDiagnostics(snapshot);
2956
+ if (push.status === "ready")
2957
+ return { items: [...push.diagnostics] };
2958
+ let pushFallbackOnly = !this.isDiagnosticPullSupported();
2959
+ if (!pushFallbackOnly) {
2960
+ const cached = this.documents.getPullCache(snapshot);
2961
+ try {
2962
+ const remainingMs2 = deadlineAt - Date.now();
2963
+ if (remainingMs2 <= 0)
2964
+ return this.freshnessTimeout(absPath);
2965
+ const result = await this.sendRequest("textDocument/diagnostic", {
2966
+ textDocument: { uri },
2967
+ ...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
2968
+ }, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
2969
+ if (!this.documents.isCurrentSnapshot(snapshot))
2970
+ continue;
2971
+ const report = this.parseDiagnosticPullReport(result);
2972
+ if (report.type === "full") {
2973
+ this.documents.recordPullDiagnostics(snapshot, {
2974
+ kind: "full",
2975
+ diagnostics: report.diagnostics,
2976
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
2977
+ });
2978
+ return { items: [...report.diagnostics] };
2979
+ }
2980
+ if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
2981
+ return { items: [...cached.diagnostics] };
2982
+ }
2983
+ } catch (error) {
2984
+ if (this.isUnsupportedDiagnosticPullError(error)) {
2985
+ this.setDiagnosticPullSupported(false);
2986
+ pushFallbackOnly = true;
2987
+ } else if (error instanceof LspRequestTimeoutError) {
2988
+ pushFallbackOnly = true;
2989
+ } else {
2990
+ this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2991
+ throw error;
2992
+ }
2993
+ }
2994
+ }
2995
+ if (!pushFallbackOnly)
2996
+ continue;
2997
+ const remainingMs = deadlineAt - Date.now();
2998
+ if (remainingMs <= 0)
2999
+ return this.freshnessTimeout(absPath);
3000
+ const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
3001
+ await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
3002
+ }
3003
+ }
3004
+ async prepareRename(filePath, line, character, signal) {
3005
+ const absPath = this.resolveWorkspacePath(filePath);
3006
+ await this.openFile(absPath);
3007
+ const options = signal === undefined ? {} : { signal };
3008
+ return this.sendRequest("textDocument/prepareRename", {
3009
+ textDocument: { uri: pathToFileURL3(absPath).href },
3010
+ position: { line: line - 1, character }
3011
+ }, options);
3012
+ }
3013
+ async rename(filePath, line, character, newName, signal) {
3014
+ const absPath = this.resolveWorkspacePath(filePath);
3015
+ await this.openFile(absPath);
3016
+ const acquired = this.workspaceMutations.acquire(signal);
3017
+ if (!acquired.success)
3018
+ return { edit: null, apply: acquired.result };
3019
+ const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
3020
+ try {
3021
+ const renameParams = {
3022
+ textDocument: { uri: pathToFileURL3(absPath).href },
3023
+ position: { line: line - 1, character },
3024
+ newName
3025
+ };
3026
+ const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
3027
+ signal: preCommitSignal.signal
3028
+ });
3029
+ return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
3030
+ } finally {
3031
+ preCommitSignal?.dispose();
3032
+ this.workspaceMutations.release(acquired.lease);
3033
+ }
3034
+ }
3035
+ resolveWorkspacePath(filePath) {
3036
+ return resolve6(this.root, filePath);
3037
+ }
3038
+ }
3039
+ function waitForDiagnosticsActivity(wait, signal) {
3040
+ if (!signal)
3041
+ return wait;
3042
+ if (signal.aborted)
3043
+ return Promise.reject(abortError2(signal));
3044
+ return new Promise((resolve7, reject) => {
3045
+ const onAbort = () => {
3046
+ signal.removeEventListener("abort", onAbort);
3047
+ reject(abortError2(signal));
3048
+ };
3049
+ signal.addEventListener("abort", onAbort, { once: true });
3050
+ wait.then(() => {
3051
+ signal.removeEventListener("abort", onAbort);
3052
+ resolve7();
3053
+ }, (error) => {
3054
+ signal.removeEventListener("abort", onAbort);
3055
+ reject(error);
3056
+ });
3057
+ });
3058
+ }
3059
+ function createPreCommitAbortSignal(source, isBeforeCommit) {
3060
+ if (!source)
3061
+ return;
3062
+ const controller = new AbortController;
3063
+ const onAbort = () => {
3064
+ if (isBeforeCommit() && !controller.signal.aborted)
3065
+ controller.abort(preCommitAbortReason(source));
3066
+ };
3067
+ if (source.aborted)
3068
+ onAbort();
3069
+ else
3070
+ source.addEventListener("abort", onAbort, { once: true });
3071
+ return {
3072
+ signal: controller.signal,
3073
+ dispose: () => source.removeEventListener("abort", onAbort)
3074
+ };
3075
+ }
3076
+ function preCommitAbortReason(source) {
3077
+ const reason = source.reason;
3078
+ if (reason instanceof Error && reason.name !== "AbortError")
3079
+ return reason;
3080
+ return new Error("LSP request cancelled before workspace edit commit");
3081
+ }
3082
+ function abortError2(signal) {
3083
+ const reason = signal.reason;
3084
+ if (reason instanceof Error)
3085
+ return reason;
3086
+ const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
3087
+ error.name = "AbortError";
3088
+ return error;
3089
+ }
3090
+
3091
+ // ../lsp-core/src/lsp/process-signal-cleanup.ts
3092
+ function installProcessSignalCleanup(cleanup) {
3093
+ const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
3094
+ const handler = () => {
3095
+ cleanup().catch((error) => {
3096
+ reportBestEffortCleanupError("signal cleanup", error);
3097
+ });
3098
+ };
3099
+ for (const signal of signals) {
3100
+ process.on(signal, handler);
3101
+ }
3102
+ return () => {
3103
+ for (const signal of signals) {
3104
+ process.removeListener(signal, handler);
3105
+ }
3106
+ };
3107
+ }
3108
+
3109
+ // ../lsp-core/src/lsp/manager.ts
3110
+ async function stopClientBestEffort(client) {
3111
+ try {
3112
+ await client.stop();
3113
+ } catch (error) {
3114
+ reportBestEffortCleanupError("client stop", error);
3115
+ }
3116
+ }
3117
+ function awaitWithSignal(promise, signal) {
3118
+ if (!signal)
3119
+ return promise;
3120
+ return new Promise((resolve7, reject) => {
3121
+ let settled = false;
3122
+ const onAbort = () => {
3123
+ if (settled)
3124
+ return;
3125
+ settled = true;
1366
3126
  reject(new DOMException("Aborted", "AbortError"));
1367
3127
  };
1368
3128
  if (signal.aborted) {
@@ -1375,7 +3135,7 @@ function awaitWithSignal(promise, signal) {
1375
3135
  return;
1376
3136
  settled = true;
1377
3137
  signal.removeEventListener("abort", onAbort);
1378
- resolve2(value);
3138
+ resolve7(value);
1379
3139
  }, (err) => {
1380
3140
  if (settled)
1381
3141
  return;
@@ -1629,21 +3389,17 @@ async function disposeDefaultLspManager() {
1629
3389
  }
1630
3390
 
1631
3391
  // ../lsp-core/src/lsp/server-install-state.ts
1632
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
1633
- import { homedir } from "node:os";
1634
- import { dirname, isAbsolute, join as join2 } from "node:path";
3392
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3393
+ import { dirname as dirname5 } from "node:path";
1635
3394
  function getInstallDecisionsPath() {
1636
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
1637
- if (!override)
1638
- return join2(homedir(), ".codex", "lsp-install-decisions.json");
1639
- return isAbsolute(override) ? override : join2(homedir(), override);
3395
+ return lspRequestContext().installDecisionsPath;
1640
3396
  }
1641
3397
  function loadInstallDecisions() {
1642
3398
  const path = getInstallDecisionsPath();
1643
- if (!existsSync2(path))
3399
+ if (!existsSync6(path))
1644
3400
  return {};
1645
3401
  try {
1646
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
3402
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1647
3403
  return isInstallDecisions(parsed) ? parsed : {};
1648
3404
  } catch {
1649
3405
  return {};
@@ -1662,28 +3418,26 @@ function isInstallDecision(value) {
1662
3418
  }
1663
3419
  function writeInstallDecisions(decisions) {
1664
3420
  const path = getInstallDecisionsPath();
1665
- mkdirSync(dirname(path), { recursive: true });
3421
+ mkdirSync(dirname5(path), { recursive: true });
1666
3422
  const tmpPath = `${path}.tmp`;
1667
- writeFileSync(tmpPath, `${JSON.stringify(decisions, null, 2)}
3423
+ writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
1668
3424
  `, "utf8");
1669
- renameSync(tmpPath, path);
3425
+ renameSync2(tmpPath, path);
1670
3426
  }
1671
3427
  function isInstallDecisions(value) {
1672
- return isRecord2(value) && Object.values(value).every(isInstallDecisionRecord);
3428
+ return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
1673
3429
  }
1674
3430
  function isInstallDecisionRecord(value) {
1675
- if (!isRecord2(value))
3431
+ if (!isRecord5(value))
1676
3432
  return false;
1677
3433
  return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
1678
3434
  }
1679
- function isRecord2(value) {
3435
+ function isRecord5(value) {
1680
3436
  return typeof value === "object" && value !== null && !Array.isArray(value);
1681
3437
  }
1682
3438
 
1683
3439
  // ../lsp-core/src/lsp/config-loader.ts
1684
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1685
- import { homedir as homedir2 } from "node:os";
1686
- import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
3440
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
1687
3441
 
1688
3442
  // ../lsp-core/src/lsp/server-definitions.ts
1689
3443
  var LSP_INSTALL_HINTS = {
@@ -1833,27 +3587,17 @@ var BUILTIN_SERVERS = {
1833
3587
  };
1834
3588
 
1835
3589
  // ../lsp-core/src/lsp/config-loader.ts
1836
- function resolveProjectConfigPath(path) {
1837
- return isAbsolute2(path) ? path : join3(contextCwd(), path);
1838
- }
1839
3590
  function getProjectConfigPaths() {
1840
- const projectOverride = contextEnv("LSP_TOOLS_MCP_PROJECT_CONFIG");
1841
- if (projectOverride) {
1842
- return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
1843
- }
1844
- return [join3(contextCwd(), ".codex", "lsp-client.json")];
3591
+ return lspRequestContext().projectConfigPaths;
1845
3592
  }
1846
3593
  function getUserConfigPath() {
1847
- const userOverride = contextEnv("LSP_TOOLS_MCP_USER_CONFIG");
1848
- if (!userOverride)
1849
- return join3(homedir2(), ".codex", "lsp-client.json");
1850
- return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
3594
+ return lspRequestContext().userConfigPath;
1851
3595
  }
1852
3596
  function loadJsonFile(path) {
1853
- if (!existsSync3(path))
3597
+ if (!existsSync7(path))
1854
3598
  return null;
1855
3599
  try {
1856
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3600
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
1857
3601
  return isConfigJson(parsed) ? parsed : null;
1858
3602
  } catch {
1859
3603
  return null;
@@ -1992,16 +3736,16 @@ function applyOptionalServerFields(server2, entry) {
1992
3736
  }
1993
3737
  }
1994
3738
  function isConfigJson(value) {
1995
- if (!isRecord3(value))
3739
+ if (!isRecord6(value))
1996
3740
  return false;
1997
3741
  const lsp = value["lsp"];
1998
- return lsp === undefined || isRecord3(lsp);
3742
+ return lsp === undefined || isRecord6(lsp);
1999
3743
  }
2000
3744
  function parseLspEntry(value) {
2001
3745
  return isLspEntry(value) ? value : null;
2002
3746
  }
2003
3747
  function isLspEntry(value) {
2004
- if (!isRecord3(value))
3748
+ if (!isRecord6(value))
2005
3749
  return false;
2006
3750
  const disabled = value["disabled"];
2007
3751
  const command = value["command"];
@@ -2009,15 +3753,15 @@ function isLspEntry(value) {
2009
3753
  const priority = value["priority"];
2010
3754
  const env = value["env"];
2011
3755
  const initialization = value["initialization"];
2012
- 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));
3756
+ 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));
2013
3757
  }
2014
3758
  function isStringArray(value) {
2015
3759
  return Array.isArray(value) && value.every((item) => typeof item === "string");
2016
3760
  }
2017
3761
  function isStringRecord(value) {
2018
- return isRecord3(value) && Object.values(value).every((item) => typeof item === "string");
3762
+ return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
2019
3763
  }
2020
- function isRecord3(value) {
3764
+ function isRecord6(value) {
2021
3765
  return typeof value === "object" && value !== null && !Array.isArray(value);
2022
3766
  }
2023
3767
  function getDisabledServerIds() {
@@ -2038,8 +3782,8 @@ function getDisabledServerIds() {
2038
3782
  }
2039
3783
 
2040
3784
  // ../lsp-core/src/lsp/server-installation.ts
2041
- import { existsSync as existsSync4 } from "node:fs";
2042
- import { delimiter as delimiter3, join as join4 } from "node:path";
3785
+ import { existsSync as existsSync8 } from "node:fs";
3786
+ import { delimiter as delimiter3, join as join3 } from "node:path";
2043
3787
  function isServerInstalled(command, _workingDirectory) {
2044
3788
  if (command.length === 0)
2045
3789
  return false;
@@ -2047,7 +3791,7 @@ function isServerInstalled(command, _workingDirectory) {
2047
3791
  if (!cmd)
2048
3792
  return false;
2049
3793
  if (cmd.includes("/") || cmd.includes("\\")) {
2050
- if (existsSync4(cmd))
3794
+ if (existsSync8(cmd))
2051
3795
  return true;
2052
3796
  }
2053
3797
  const isWindows = process.platform === "win32";
@@ -2068,7 +3812,7 @@ function isServerInstalled(command, _workingDirectory) {
2068
3812
  const paths = pathEnv.split(delimiter3);
2069
3813
  for (const p of paths) {
2070
3814
  for (const suffix of exts) {
2071
- if (existsSync4(join4(p, cmd + suffix))) {
3815
+ if (existsSync8(join3(p, cmd + suffix))) {
2072
3816
  return true;
2073
3817
  }
2074
3818
  }
@@ -2167,39 +3911,50 @@ function getAllServers() {
2167
3911
  var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2168
3912
  function isDirectoryPath(filePath) {
2169
3913
  try {
2170
- return statSync2(filePath).isDirectory();
3914
+ return statSync3(filePath).isDirectory();
2171
3915
  } catch {
2172
3916
  return false;
2173
3917
  }
2174
3918
  }
2175
3919
  function findWorkspaceRoot(filePath) {
2176
- const abs = resolve2(contextCwd(), filePath);
3920
+ const abs = resolvePathInsideContext(filePath);
2177
3921
  let dir = abs;
2178
3922
  if (!isDirectoryPath(dir)) {
2179
- dir = dirname2(dir);
3923
+ dir = dirname6(dir);
2180
3924
  }
2181
3925
  let prevDir = "";
2182
3926
  while (dir !== prevDir) {
2183
3927
  for (const marker of WORKSPACE_MARKERS) {
2184
- if (existsSync5(join5(dir, marker))) {
3928
+ if (existsSync9(join4(dir, marker))) {
2185
3929
  return dir;
2186
3930
  }
2187
3931
  }
2188
3932
  prevDir = dir;
2189
- dir = dirname2(dir);
3933
+ dir = dirname6(dir);
2190
3934
  }
2191
- return dirname2(abs);
3935
+ return dirname6(abs);
3936
+ }
3937
+ function resolvePathInsideContext(filePath) {
3938
+ const cwd = contextCwd();
3939
+ const abs = resolve7(cwd, filePath);
3940
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
3941
+ if (!isPathInside(cwd, canonical)) {
3942
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
3943
+ }
3944
+ return canonical;
2192
3945
  }
2193
3946
  function formatServerLookupError(result) {
2194
3947
  if (result.status === "not_installed") {
2195
3948
  return formatNotInstalled(result);
2196
3949
  }
3950
+ const context = lspRequestContext();
3951
+ const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
2197
3952
  return [
2198
3953
  `No LSP server configured for extension: ${result.extension}`,
2199
3954
  "",
2200
3955
  `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
2201
3956
  "",
2202
- "Configure a custom server in '.codex/lsp-client.json':",
3957
+ `Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
2203
3958
  " {",
2204
3959
  ' "lsp": {',
2205
3960
  ' "my-server": {',
@@ -2215,6 +3970,7 @@ function formatNotInstalled(result) {
2215
3970
  const { server: server2, installHint } = result;
2216
3971
  const extensions = server2.extensions.join(", ");
2217
3972
  const decision = loadInstallDecision(server2.id)?.decision;
3973
+ const context = lspRequestContext();
2218
3974
  if (decision === "declined") {
2219
3975
  return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
2220
3976
  }
@@ -2230,6 +3986,17 @@ function formatNotInstalled(result) {
2230
3986
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
2231
3987
  ` ${installHint}`
2232
3988
  ].join(`
3989
+ `);
3990
+ }
3991
+ if (!context.capabilities.installDecisionTool) {
3992
+ return [
3993
+ ...header,
3994
+ "To install, run:",
3995
+ ` ${installHint}`,
3996
+ "",
3997
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
3998
+ "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
3999
+ ].join(`
2233
4000
  `);
2234
4001
  }
2235
4002
  return [
@@ -2241,6 +4008,7 @@ function formatNotInstalled(result) {
2241
4008
  "- If the user agrees: run the install command above, then retry this tool.",
2242
4009
  "- If the user declines, OR has NOT explicitly asked for LSP installation:",
2243
4010
  ` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
4011
+ ` which writes to ${context.installDecisionsPath},`,
2244
4012
  " then ignore this message and proceed WITHOUT LSP."
2245
4013
  ].join(`
2246
4014
  `);
@@ -2254,14 +4022,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
2254
4022
  "prepareRename"
2255
4023
  ]);
2256
4024
  async function withLspClient(filePath, fn, toolName, options = {}) {
2257
- const absPath = resolve2(contextCwd(), filePath);
4025
+ const absPath = resolvePathInsideContext(filePath);
2258
4026
  if (isDirectoryPath(absPath)) {
2259
4027
  throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
2260
4028
  }
2261
4029
  const ext = effectiveExtension(absPath);
2262
4030
  const result = findServerForExtension(ext);
2263
4031
  if (result.status !== "found") {
2264
- throw new LspServerLookupError(formatServerLookupError(result));
4032
+ throw new LspServerLookupError(formatServerLookupError(result), result);
2265
4033
  }
2266
4034
  const server2 = result.server;
2267
4035
  const root = findWorkspaceRoot(absPath);
@@ -2289,11 +4057,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
2289
4057
  }
2290
4058
 
2291
4059
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2292
- import { existsSync as existsSync6, lstatSync, readdirSync } from "node:fs";
2293
- import { join as join6, resolve as resolve3 } from "node:path";
4060
+ import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
4061
+ import { join as join5, resolve as resolve8 } from "node:path";
2294
4062
 
2295
4063
  // ../lsp-core/src/lsp/formatters.ts
2296
- import { fileURLToPath } from "node:url";
4064
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2297
4065
  var DIAGNOSTIC_SEVERITY_FILTERS = {
2298
4066
  error: 1,
2299
4067
  warning: 2,
@@ -2301,7 +4069,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
2301
4069
  hint: 4
2302
4070
  };
2303
4071
  function uriToPath(uri) {
2304
- return fileURLToPath(uri);
4072
+ return fileURLToPath2(uri);
2305
4073
  }
2306
4074
  function formatLocation(loc) {
2307
4075
  if ("targetUri" in loc) {
@@ -2387,6 +4155,9 @@ function formatApplyResult(result) {
2387
4155
  for (const file of result.filesModified) {
2388
4156
  lines.push(` - ${file}`);
2389
4157
  }
4158
+ if (result.lateAbort) {
4159
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
4160
+ }
2390
4161
  } else {
2391
4162
  lines.push("Failed to apply some changes:");
2392
4163
  for (const err of result.errors) {
@@ -2402,6 +4173,7 @@ function formatApplyResult(result) {
2402
4173
 
2403
4174
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2404
4175
  var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
4176
+ var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
2405
4177
  function collectFilesWithExtension(dir, extension, maxFiles) {
2406
4178
  const files = [];
2407
4179
  function walk(currentDir) {
@@ -2409,17 +4181,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2409
4181
  return;
2410
4182
  let entries = [];
2411
4183
  try {
2412
- entries = readdirSync(currentDir);
4184
+ entries = readdirSync3(currentDir);
2413
4185
  } catch {
2414
4186
  return;
2415
4187
  }
2416
4188
  for (const entry of entries) {
2417
4189
  if (files.length >= maxFiles)
2418
4190
  return;
2419
- const fullPath = join6(currentDir, entry);
4191
+ const fullPath = join5(currentDir, entry);
2420
4192
  let stat;
2421
4193
  try {
2422
- stat = lstatSync(fullPath);
4194
+ stat = lstatSync4(fullPath);
2423
4195
  } catch {
2424
4196
  continue;
2425
4197
  }
@@ -2437,52 +4209,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2437
4209
  walk(dir);
2438
4210
  return files;
2439
4211
  }
2440
- async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
4212
+ async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
2441
4213
  if (!extension.startsWith(".")) {
2442
4214
  throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
2443
4215
  }
2444
- const absDir = resolve3(contextCwd(), directory);
2445
- if (!existsSync6(absDir)) {
4216
+ const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
4217
+ if (!existsSync10(absDir)) {
2446
4218
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
2447
4219
  }
2448
- const serverResult = findServerForExtension(extension);
4220
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
2449
4221
  if (serverResult.status !== "found") {
2450
4222
  throw new LspServerLookupError(formatServerLookupError(serverResult));
2451
4223
  }
2452
4224
  const server2 = serverResult.server;
2453
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
4225
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
2454
4226
  const wasCapped = allFiles.length > maxFiles;
2455
4227
  const filesToProcess = allFiles.slice(0, maxFiles);
2456
4228
  if (filesToProcess.length === 0) {
2457
- return [
4229
+ const output = [
2458
4230
  `Directory: ${absDir}`,
2459
4231
  `Extension: ${extension}`,
2460
4232
  "Files scanned: 0",
2461
4233
  `No files found with extension "${extension}".`
2462
4234
  ].join(`
2463
4235
  `);
4236
+ return { output, totalDiagnostics: 0, fileFailures: [] };
2464
4237
  }
2465
- const root = findWorkspaceRoot(absDir);
2466
- const manager = getLspManager();
4238
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
4239
+ const manager = options.manager ?? getLspManager();
2467
4240
  const allDiagnostics = [];
2468
4241
  const fileErrors = [];
4242
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
4243
+ options.signal?.throwIfAborted();
2469
4244
  const client = await manager.getClient(root, server2);
2470
4245
  try {
2471
- for (const file of filesToProcess) {
2472
- try {
2473
- const result = await client.diagnostics(file);
2474
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
2475
- allDiagnostics.push(...filtered.map((diagnostic) => ({
2476
- filePath: file,
2477
- diagnostic
2478
- })));
2479
- } catch (e) {
2480
- fileErrors.push({
2481
- file,
2482
- error: e instanceof Error ? e.message : String(e)
2483
- });
4246
+ let nextIndex = 0;
4247
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
4248
+ for (;; ) {
4249
+ if (options.signal?.aborted)
4250
+ return;
4251
+ const file = filesToProcess[nextIndex];
4252
+ nextIndex += 1;
4253
+ if (file === undefined)
4254
+ return;
4255
+ try {
4256
+ const result = await client.diagnostics(file, options.signal);
4257
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
4258
+ allDiagnostics.push(...filtered.map((diagnostic) => ({
4259
+ filePath: file,
4260
+ diagnostic
4261
+ })));
4262
+ } catch (e) {
4263
+ fileErrors.push({
4264
+ file,
4265
+ error: e instanceof Error ? e.message : String(e)
4266
+ });
4267
+ }
2484
4268
  }
2485
- }
4269
+ });
4270
+ await Promise.all(workers);
2486
4271
  } finally {
2487
4272
  manager.releaseClient(root, server2.id);
2488
4273
  }
@@ -2510,13 +4295,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
2510
4295
  lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
2511
4296
  }
2512
4297
  }
2513
- return lines.join(`
2514
- `);
4298
+ return { output: lines.join(`
4299
+ `), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
2515
4300
  }
2516
4301
 
2517
4302
  // ../lsp-core/src/lsp/infer-extension.ts
2518
- import { lstatSync as lstatSync2, readdirSync as readdirSync2 } from "node:fs";
2519
- import { join as join7 } from "node:path";
4303
+ import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
4304
+ import { join as join6 } from "node:path";
2520
4305
  var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
2521
4306
  var MAX_SCAN_ENTRIES = 500;
2522
4307
  function inferExtensionFromDirectory(directory) {
@@ -2527,17 +4312,17 @@ function inferExtensionFromDirectory(directory) {
2527
4312
  return;
2528
4313
  let entries;
2529
4314
  try {
2530
- entries = readdirSync2(dir);
4315
+ entries = readdirSync4(dir);
2531
4316
  } catch {
2532
4317
  return;
2533
4318
  }
2534
4319
  for (const entry of entries) {
2535
4320
  if (scanned >= MAX_SCAN_ENTRIES)
2536
4321
  return;
2537
- const fullPath = join7(dir, entry);
4322
+ const fullPath = join6(dir, entry);
2538
4323
  let stat;
2539
4324
  try {
2540
- stat = lstatSync2(fullPath);
4325
+ stat = lstatSync5(fullPath);
2541
4326
  } catch {
2542
4327
  continue;
2543
4328
  }
@@ -2611,13 +4396,48 @@ function missingDependencyResult(error, details) {
2611
4396
  details: {
2612
4397
  ...details,
2613
4398
  error: message,
2614
- errorKind: "missing_dependency"
4399
+ errorKind: "missing_dependency",
4400
+ ...availabilityDetails(error)
2615
4401
  }
2616
4402
  };
2617
4403
  }
4404
+ function availabilityDetails(error) {
4405
+ const availability = missingDependencyAvailability(error);
4406
+ return availability === null ? {} : { availability };
4407
+ }
4408
+ function missingDependencyAvailability(error) {
4409
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
4410
+ return null;
4411
+ const context = lspRequestContext();
4412
+ switch (error.lookup.status) {
4413
+ case "not_configured":
4414
+ return {
4415
+ kind: "not_configured",
4416
+ extension: error.lookup.extension,
4417
+ availableServers: [...error.lookup.availableServers],
4418
+ projectConfigPaths: [...context.projectConfigPaths],
4419
+ userConfigPath: context.userConfigPath,
4420
+ installDecisionTool: context.capabilities.installDecisionTool
4421
+ };
4422
+ case "not_installed":
4423
+ return {
4424
+ kind: "not_installed",
4425
+ serverId: error.lookup.server.id,
4426
+ command: [...error.lookup.server.command],
4427
+ extensions: [...error.lookup.server.extensions],
4428
+ installHint: error.lookup.installHint,
4429
+ installDecisionTool: context.capabilities.installDecisionTool,
4430
+ installDecisionsPath: context.installDecisionsPath
4431
+ };
4432
+ default: {
4433
+ const exhaustive = error.lookup;
4434
+ return exhaustive;
4435
+ }
4436
+ }
4437
+ }
2618
4438
 
2619
4439
  // ../lsp-core/src/tools/parameters.ts
2620
- function isRecord4(value) {
4440
+ function isRecord7(value) {
2621
4441
  return typeof value === "object" && value !== null && !Array.isArray(value);
2622
4442
  }
2623
4443
  function requireString(params, key) {
@@ -2674,7 +4494,7 @@ async function executeLspDiagnostics(params, signal) {
2674
4494
  const filePath = requireString(params, "filePath");
2675
4495
  const severity = severityFilter(params);
2676
4496
  try {
2677
- const absPath = resolve4(contextCwd(), filePath);
4497
+ const absPath = resolvePathInsideContext(filePath);
2678
4498
  if (isDirectoryPath(absPath)) {
2679
4499
  const extension = inferExtensionFromDirectory(absPath);
2680
4500
  if (!extension) {
@@ -2691,18 +4511,33 @@ async function executeLspDiagnostics(params, signal) {
2691
4511
  };
2692
4512
  return text(message, details3);
2693
4513
  }
2694
- const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
4514
+ const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
2695
4515
  const details2 = {
2696
4516
  filePath,
2697
4517
  severity,
2698
4518
  mode: "directory",
2699
4519
  diagnostics: [],
4520
+ totalDiagnostics: output2.totalDiagnostics,
4521
+ truncated: false,
4522
+ fileFailures: [...output2.fileFailures]
4523
+ };
4524
+ return text(output2.output, details2);
4525
+ }
4526
+ const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
4527
+ if (result.transientError) {
4528
+ const message = result.transientError.message;
4529
+ const details2 = {
4530
+ filePath,
4531
+ severity,
4532
+ mode: "file",
4533
+ diagnostics: [],
2700
4534
  totalDiagnostics: 0,
2701
- truncated: false
4535
+ truncated: false,
4536
+ error: message,
4537
+ errorKind: result.transientError.kind
2702
4538
  };
2703
- return text(output2, details2);
4539
+ return text(message, details2, true);
2704
4540
  }
2705
- const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
2706
4541
  const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
2707
4542
  const total = diagnostics.length;
2708
4543
  const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
@@ -2763,7 +4598,7 @@ async function executeLspGotoDefinition(params, signal) {
2763
4598
  const line = requireNumber(params, "line");
2764
4599
  const character = requireNumber(params, "character");
2765
4600
  try {
2766
- const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
4601
+ const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
2767
4602
  const locations = !result ? [] : Array.isArray(result) ? result : [result];
2768
4603
  const details = { filePath, line, character, locations };
2769
4604
  if (locations.length === 0)
@@ -2788,7 +4623,7 @@ async function executeLspFindReferences(params, signal) {
2788
4623
  const character = requireNumber(params, "character");
2789
4624
  const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
2790
4625
  try {
2791
- const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
4626
+ const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
2792
4627
  const references = Array.isArray(result) ? result : [];
2793
4628
  const total = references.length;
2794
4629
  const truncated = total > DEFAULT_MAX_REFERENCES;
@@ -2824,181 +4659,13 @@ async function executeLspFindReferences(params, signal) {
2824
4659
  }
2825
4660
  }
2826
4661
 
2827
- // ../lsp-core/src/lsp/workspace-edit.ts
2828
- import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2829
- import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
2830
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2831
- function errorMessage2(error) {
2832
- return error instanceof Error ? error.message : String(error);
2833
- }
2834
- function isPathInsideWorkspace(filePath, workspaceRoot) {
2835
- const relativePath = relative(workspaceRoot, filePath);
2836
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
2837
- }
2838
- function realpathForValidation(filePath) {
2839
- if (existsSync7(filePath))
2840
- return realpathSync(filePath);
2841
- const parent = dirname3(filePath);
2842
- return resolve5(realpathSync(parent), relative(parent, filePath));
2843
- }
2844
- function uriToWorkspacePath(uri, workspaceRoot) {
2845
- let filePath;
2846
- try {
2847
- filePath = fileURLToPath2(uri);
2848
- } catch (error) {
2849
- return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
2850
- }
2851
- let validatedPath;
2852
- try {
2853
- validatedPath = realpathForValidation(filePath);
2854
- } catch (error) {
2855
- return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
2856
- }
2857
- if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
2858
- return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
2859
- }
2860
- return { success: true, path: filePath };
2861
- }
2862
- function applyTextEditsToFile(filePath, edits) {
2863
- try {
2864
- const content = readFileSync4(filePath, "utf-8");
2865
- const lines = content.split(`
2866
- `);
2867
- const sortedEdits = [...edits].sort((a, b) => {
2868
- if (b.range.start.line !== a.range.start.line) {
2869
- return b.range.start.line - a.range.start.line;
2870
- }
2871
- return b.range.start.character - a.range.start.character;
2872
- });
2873
- for (const edit of sortedEdits) {
2874
- const startLine = edit.range.start.line;
2875
- const startChar = edit.range.start.character;
2876
- const endLine = edit.range.end.line;
2877
- const endChar = edit.range.end.character;
2878
- if (startLine === endLine) {
2879
- const line = lines[startLine] ?? "";
2880
- lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
2881
- } else {
2882
- const firstLine = lines[startLine] ?? "";
2883
- const lastLine = lines[endLine] ?? "";
2884
- const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
2885
- lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
2886
- `));
2887
- }
2888
- }
2889
- writeFileSync2(filePath, lines.join(`
2890
- `), "utf-8");
2891
- return { success: true, editCount: edits.length };
2892
- } catch (err) {
2893
- return {
2894
- success: false,
2895
- editCount: 0,
2896
- error: err instanceof Error ? err.message : String(err)
2897
- };
2898
- }
2899
- }
2900
- function applyWorkspaceEdit(edit, options = {}) {
2901
- if (!edit) {
2902
- return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
2903
- }
2904
- const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
2905
- const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
2906
- if (edit.changes) {
2907
- for (const [uri, edits] of Object.entries(edit.changes)) {
2908
- const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
2909
- if (!validatedPath.success) {
2910
- result.success = false;
2911
- result.errors.push(validatedPath.error);
2912
- continue;
2913
- }
2914
- const applyResult = applyTextEditsToFile(validatedPath.path, edits);
2915
- if (applyResult.success) {
2916
- result.filesModified.push(validatedPath.path);
2917
- result.totalEdits += applyResult.editCount;
2918
- } else {
2919
- result.success = false;
2920
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2921
- }
2922
- }
2923
- }
2924
- if (edit.documentChanges) {
2925
- for (const change of edit.documentChanges) {
2926
- if (!("kind" in change)) {
2927
- const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
2928
- if (!validatedPath.success) {
2929
- result.success = false;
2930
- result.errors.push(validatedPath.error);
2931
- continue;
2932
- }
2933
- const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
2934
- if (applyResult.success) {
2935
- result.filesModified.push(validatedPath.path);
2936
- result.totalEdits += applyResult.editCount;
2937
- } else {
2938
- result.success = false;
2939
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2940
- }
2941
- continue;
2942
- }
2943
- if (change.kind === "create") {
2944
- try {
2945
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2946
- if (!validatedPath.success) {
2947
- result.success = false;
2948
- result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
2949
- continue;
2950
- }
2951
- writeFileSync2(validatedPath.path, "", "utf-8");
2952
- result.filesModified.push(validatedPath.path);
2953
- } catch (err) {
2954
- result.success = false;
2955
- result.errors.push(`Create ${change.uri}: ${String(err)}`);
2956
- }
2957
- } else if (change.kind === "rename") {
2958
- try {
2959
- const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
2960
- const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
2961
- if (!oldPath.success || !newPath.success) {
2962
- const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
2963
- result.success = false;
2964
- result.errors.push(`Rename ${change.oldUri}: ${error}`);
2965
- continue;
2966
- }
2967
- const content = readFileSync4(oldPath.path, "utf-8");
2968
- writeFileSync2(newPath.path, content, "utf-8");
2969
- unlinkSync(oldPath.path);
2970
- result.filesModified.push(newPath.path);
2971
- } catch (err) {
2972
- result.success = false;
2973
- result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
2974
- }
2975
- } else if (change.kind === "delete") {
2976
- try {
2977
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2978
- if (!validatedPath.success) {
2979
- result.success = false;
2980
- result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
2981
- continue;
2982
- }
2983
- unlinkSync(validatedPath.path);
2984
- result.filesModified.push(validatedPath.path);
2985
- } catch (err) {
2986
- result.success = false;
2987
- result.errors.push(`Delete ${change.uri}: ${String(err)}`);
2988
- }
2989
- }
2990
- }
2991
- }
2992
- return result;
2993
- }
2994
-
2995
4662
  // ../lsp-core/src/tools/rename.ts
2996
4663
  async function executeLspPrepareRename(params, signal) {
2997
4664
  const filePath = requireString(params, "filePath");
2998
4665
  const line = requireNumber(params, "line");
2999
4666
  const character = requireNumber(params, "character");
3000
4667
  try {
3001
- const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
4668
+ const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
3002
4669
  const details = { filePath, line, character, result };
3003
4670
  return text(formatPrepareRenameResult(result), details);
3004
4671
  } catch (error) {
@@ -3019,13 +4686,9 @@ async function executeLspRename(params, signal) {
3019
4686
  const character = requireNumber(params, "character");
3020
4687
  const newName = requireString(params, "newName");
3021
4688
  try {
3022
- const edit = await withLspClient(filePath, async (client, workspaceRoot) => ({
3023
- edit: await client.rename(filePath, line, character, newName),
3024
- workspaceRoot
3025
- }), "rename", clientOptions(signal));
3026
- const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
3027
- const details = { filePath, line, character, newName, apply, edit: edit.edit };
3028
- return text(formatApplyResult(apply), details, !apply.success);
4689
+ const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
4690
+ const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
4691
+ return text(formatApplyResult(result.apply), details, !result.apply.success);
3029
4692
  } catch (error) {
3030
4693
  const missingDependency = missingDependencyResult(error, {
3031
4694
  filePath,
@@ -3100,10 +4763,10 @@ async function executeLspSymbols(params, signal) {
3100
4763
  errorKind: "missing_query"
3101
4764
  });
3102
4765
  }
3103
- const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
4766
+ const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
3104
4767
  return formatSymbolsResult(filePath, scope, symbols2, limit, query);
3105
4768
  }
3106
- const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
4769
+ const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
3107
4770
  return formatSymbolsResult(filePath, scope, symbols, limit);
3108
4771
  } catch (error) {
3109
4772
  const query = optionalString(params, "query");
@@ -3269,12 +4932,12 @@ function matchesToolName(tool, name) {
3269
4932
  return tool.name === name || (tool.aliases?.includes(name) ?? false);
3270
4933
  }
3271
4934
  function coerceToolArguments(value) {
3272
- return isRecord4(value) ? value : {};
4935
+ return isRecord7(value) ? value : {};
3273
4936
  }
3274
4937
  // ../lsp-core/src/mcp.ts
3275
4938
  var SERVER_NAME = "lsp";
3276
4939
  var SERVER_VERSION = "0.1.0";
3277
- async function handleLspMcpRequest(input) {
4940
+ async function handleLspMcpRequest(input, options = {}) {
3278
4941
  if (!isPlainRecord(input)) {
3279
4942
  return errorResponse(null, -32600, "Invalid Request");
3280
4943
  }
@@ -3296,24 +4959,27 @@ async function handleLspMcpRequest(input) {
3296
4959
  return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
3297
4960
  }
3298
4961
  if (method === "tools/call") {
3299
- return handleToolCall(id, input["params"]);
4962
+ return handleToolCall(id, input["params"], options.signal);
3300
4963
  }
3301
4964
  return errorResponse(id, -32601, `Method not found: ${String(method)}`);
3302
4965
  }
3303
- async function handleToolCall(id, params) {
4966
+ async function handleToolCall(id, params, signal) {
3304
4967
  if (!isPlainRecord(params) || typeof params["name"] !== "string") {
3305
4968
  return errorResponse(id, -32602, "tools/call requires params.name");
3306
4969
  }
3307
4970
  try {
3308
- const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
4971
+ const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
3309
4972
  return successResponse(id, {
3310
4973
  content: result.content,
3311
4974
  isError: result.isError ?? false,
3312
4975
  details: result.details
3313
4976
  });
3314
4977
  } catch (error) {
4978
+ if (!(error instanceof Error)) {
4979
+ throw error;
4980
+ }
3315
4981
  return successResponse(id, {
3316
- content: [{ type: "text", text: messageFromError(error) }],
4982
+ content: [{ type: "text", text: error.message }],
3317
4983
  isError: true
3318
4984
  });
3319
4985
  }
@@ -3334,78 +5000,409 @@ function requestedProtocolVersion(params) {
3334
5000
 
3335
5001
  // src/daemon-client.ts
3336
5002
  import { connect as connect2 } from "node:net";
5003
+ import { homedir as homedir3 } from "node:os";
5004
+ import { join as join7 } from "node:path";
3337
5005
 
3338
5006
  // src/ensure-daemon.ts
3339
5007
  import { spawn as spawn2 } from "node:child_process";
3340
- import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync3, openSync as openSync2 } from "node:fs";
5008
+ import { closeSync as closeSync2, mkdirSync as mkdirSync3, openSync as openSync2 } from "node:fs";
3341
5009
  import { connect } from "node:net";
3342
- import { dirname as dirname5 } from "node:path";
5010
+ import { dirname as dirname8 } from "node:path";
3343
5011
  import { execPath } from "node:process";
3344
- import { fileURLToPath as fileURLToPath3 } from "node:url";
3345
5012
 
3346
- // src/lock.ts
3347
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeSync } from "node:fs";
3348
- import { dirname as dirname4 } from "node:path";
3349
- function isProcessAlive(pid) {
3350
- if (!Number.isInteger(pid) || pid <= 0)
3351
- return false;
5013
+ // src/ipc-protocol.ts
5014
+ import { randomBytes, timingSafeEqual } from "node:crypto";
5015
+ import {
5016
+ chmodSync,
5017
+ closeSync,
5018
+ constants,
5019
+ fchmodSync,
5020
+ fstatSync,
5021
+ lstatSync as lstatSync6,
5022
+ mkdirSync as mkdirSync2,
5023
+ openSync,
5024
+ readFileSync as readFileSync5,
5025
+ unlinkSync,
5026
+ writeSync
5027
+ } from "node:fs";
5028
+ import { dirname as dirname7 } from "node:path";
5029
+ var OMO_DAEMON_PROTOCOL_VERSION = 1;
5030
+ var AUTH_ERROR_CODE = -32001;
5031
+ var PROTOCOL_ERROR_CODE = -32002;
5032
+ var AUTH_TOKEN_BYTES = 32;
5033
+
5034
+ class UnsafePrivateDirectoryError extends Error {
5035
+ path;
5036
+ reason;
5037
+ name = "UnsafePrivateDirectoryError";
5038
+ code = "unsafe_private_directory";
5039
+ constructor(path, reason) {
5040
+ super(`unsafe private directory ${path}: ${reason}`);
5041
+ this.path = path;
5042
+ this.reason = reason;
5043
+ }
5044
+ }
5045
+ function authEnvelope(token) {
5046
+ return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
5047
+ }
5048
+ function readAuthToken(paths) {
3352
5049
  try {
3353
- process.kill(pid, 0);
3354
- return true;
5050
+ const token = readFileSync5(paths.auth, "utf8").trim();
5051
+ return token.length > 0 ? token : null;
3355
5052
  } catch (error) {
3356
- return error.code === "EPERM";
5053
+ if (error instanceof Error)
5054
+ return null;
5055
+ throw error;
3357
5056
  }
3358
5057
  }
3359
- function readLockPid(lockPath) {
5058
+ function readOrCreateAuthToken(paths) {
5059
+ const existing = readAuthToken(paths);
5060
+ if (existing)
5061
+ return existing;
5062
+ return createAuthToken(paths);
5063
+ }
5064
+ function rotateAuthToken(paths) {
3360
5065
  try {
3361
- const pid = Number.parseInt(readFileSync5(lockPath, "utf8").trim(), 10);
3362
- return Number.isInteger(pid) ? pid : null;
3363
- } catch {
3364
- return null;
5066
+ unlinkSync(paths.auth);
5067
+ } catch (error) {
5068
+ if (!(error instanceof Error))
5069
+ throw error;
3365
5070
  }
5071
+ return createAuthToken(paths);
3366
5072
  }
3367
- function tryAcquireLock(lockPath, ownerPid = process.pid) {
3368
- mkdirSync2(dirname4(lockPath), { recursive: true });
3369
- for (let attempt = 0;attempt < 2; attempt += 1) {
3370
- const handle = writeLockFile(lockPath, ownerPid);
3371
- if (handle)
3372
- return handle;
3373
- if (!reapStaleLock(lockPath))
3374
- return null;
5073
+ function authenticateMessage(raw, expectedToken) {
5074
+ const id = jsonRpcId2(raw);
5075
+ if (!isPlainRecord(raw))
5076
+ return authError(id);
5077
+ const params = raw["params"];
5078
+ if (!isPlainRecord(params))
5079
+ return authError(id);
5080
+ const envelope = params["_omo"];
5081
+ if (!isPlainRecord(envelope))
5082
+ return authError(id);
5083
+ const protocolVersion = envelope["protocolVersion"];
5084
+ if (protocolVersion !== OMO_DAEMON_PROTOCOL_VERSION)
5085
+ return protocolError(id);
5086
+ const token = envelope["token"];
5087
+ if (typeof token !== "string" || !tokenMatches(token, expectedToken))
5088
+ return authError(id);
5089
+ const cleanParams = { ...params };
5090
+ delete cleanParams["_omo"];
5091
+ return { input: { ...raw, params: cleanParams }, id, method: typeof raw["method"] === "string" ? raw["method"] : undefined };
5092
+ }
5093
+ function isAuthErrorResponse(message) {
5094
+ if (!isPlainRecord(message))
5095
+ return false;
5096
+ const error = message["error"];
5097
+ if (!isPlainRecord(error))
5098
+ return false;
5099
+ const data = error["data"];
5100
+ return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
5101
+ }
5102
+ function writePrivateFile(path, data) {
5103
+ const fd = openSync(path, "w", 384);
5104
+ try {
5105
+ writeSync(fd, data);
5106
+ } finally {
5107
+ closeSync(fd);
3375
5108
  }
3376
- return null;
5109
+ setPrivateFileMode(path);
3377
5110
  }
3378
- function writeLockFile(lockPath, ownerPid) {
5111
+ function ensurePrivateDirectory(path, options = {}) {
3379
5112
  try {
3380
- const fd = openSync(lockPath, "wx");
3381
- writeSync(fd, `${ownerPid}
3382
- `);
5113
+ mkdirSync2(path, { recursive: true, mode: 448 });
5114
+ } catch (error) {
5115
+ if (errorCode2(error) !== "EEXIST")
5116
+ throw error;
5117
+ }
5118
+ if (process.platform === "win32")
5119
+ return;
5120
+ const before = validatePrivateDirectory(path, options);
5121
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
5122
+ try {
5123
+ fchmodSync(fd, 448);
5124
+ const after = validatePrivateDirectory(path, options);
5125
+ const openStats = fstatSync(fd);
5126
+ if (!sameDirectory(before, after) || !sameDirectory(before, openStats)) {
5127
+ throw new UnsafePrivateDirectoryError(path, "changed_during_chmod");
5128
+ }
5129
+ } finally {
3383
5130
  closeSync(fd);
3384
- return { release: () => unlinkQuietly(lockPath) };
5131
+ }
5132
+ }
5133
+ function setPrivateFileMode(path) {
5134
+ if (process.platform !== "win32")
5135
+ chmodSync(path, 384);
5136
+ }
5137
+ function createAuthToken(paths) {
5138
+ ensurePrivateDirectory(dirname7(paths.auth));
5139
+ const token = randomBytes(AUTH_TOKEN_BYTES).toString("base64url");
5140
+ let fd;
5141
+ try {
5142
+ fd = openSync(paths.auth, "wx", 384);
3385
5143
  } catch (error) {
3386
- if (error.code === "EEXIST")
3387
- return null;
5144
+ if (errorCode2(error) === "EEXIST") {
5145
+ const existing = readAuthToken(paths);
5146
+ if (existing)
5147
+ return existing;
5148
+ }
3388
5149
  throw error;
3389
5150
  }
5151
+ try {
5152
+ writeSync(fd, `${token}
5153
+ `);
5154
+ } finally {
5155
+ closeSync(fd);
5156
+ }
5157
+ setPrivateFileMode(paths.auth);
5158
+ return token;
3390
5159
  }
3391
- function reapStaleLock(lockPath) {
3392
- const pid = readLockPid(lockPath);
3393
- if (pid !== null && isProcessAlive(pid))
3394
- return false;
3395
- unlinkQuietly(lockPath);
3396
- return true;
5160
+ function errorCode2(error) {
5161
+ if (!error || typeof error !== "object" || !("code" in error))
5162
+ return;
5163
+ const code = Reflect.get(error, "code");
5164
+ return typeof code === "string" ? code : undefined;
5165
+ }
5166
+ function validatePrivateDirectory(path, options) {
5167
+ const stats = options.lstat ? options.lstat(path) : lstatPrivateDirectory(path);
5168
+ if (stats.isSymbolicLink())
5169
+ throw new UnsafePrivateDirectoryError(path, "symlink");
5170
+ if (!stats.isDirectory())
5171
+ throw new UnsafePrivateDirectoryError(path, "not_directory");
5172
+ const currentUid = options.currentUid ? options.currentUid() : process.getuid?.();
5173
+ if (currentUid !== undefined && stats.uid !== currentUid) {
5174
+ throw new UnsafePrivateDirectoryError(path, "wrong_owner");
5175
+ }
5176
+ return stats;
5177
+ }
5178
+ function lstatPrivateDirectory(path) {
5179
+ return lstatSync6(path);
5180
+ }
5181
+ function sameDirectory(a, b) {
5182
+ return a.dev === b.dev && a.ino === b.ino;
5183
+ }
5184
+ function tokenMatches(candidate, expected) {
5185
+ const candidateBytes = Buffer.from(candidate);
5186
+ const expectedBytes = Buffer.from(expected);
5187
+ return candidateBytes.length === expectedBytes.length && timingSafeEqual(candidateBytes, expectedBytes);
5188
+ }
5189
+ function jsonRpcId2(raw) {
5190
+ if (!isPlainRecord(raw))
5191
+ return null;
5192
+ const id = raw["id"];
5193
+ return typeof id === "string" || typeof id === "number" || id === null ? id : null;
5194
+ }
5195
+ function authError(id) {
5196
+ return {
5197
+ jsonrpc: "2.0",
5198
+ id,
5199
+ error: { code: AUTH_ERROR_CODE, message: "daemon authentication failed", data: { code: "daemon_authentication_failed" } }
5200
+ };
5201
+ }
5202
+ function protocolError(id) {
5203
+ return {
5204
+ jsonrpc: "2.0",
5205
+ id,
5206
+ error: { code: PROTOCOL_ERROR_CODE, message: "daemon protocol mismatch", data: { code: "daemon_protocol_mismatch" } }
5207
+ };
3397
5208
  }
3398
- function unlinkQuietly(path) {
5209
+
5210
+ // src/paths.ts
5211
+ import { createHash as createHash2 } from "node:crypto";
5212
+ import { createRequire } from "node:module";
5213
+ import { homedir as homedir2, tmpdir, userInfo } from "node:os";
5214
+ import * as path from "node:path";
5215
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5216
+
5217
+ // src/runtime-contract.ts
5218
+ import { statSync as statSync4 } from "node:fs";
5219
+ import { isAbsolute as isAbsolute3 } from "node:path";
5220
+ var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
5221
+ var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
5222
+ var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
5223
+ var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
5224
+
5225
+ class InvalidRuntimeOverrideError extends Error {
5226
+ code = "invalid_runtime_override";
5227
+ reason;
5228
+ constructor(reason, message) {
5229
+ super(message);
5230
+ this.name = "InvalidRuntimeOverrideError";
5231
+ this.reason = reason;
5232
+ }
5233
+ }
5234
+
5235
+ class InvalidDaemonVersionError extends Error {
5236
+ code = "invalid_daemon_version";
5237
+ version;
5238
+ constructor(version) {
5239
+ super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
5240
+ this.name = "InvalidDaemonVersionError";
5241
+ this.version = version;
5242
+ }
5243
+ }
5244
+ function validateDaemonVersion(version) {
5245
+ if (!DAEMON_VERSION_PATTERN.test(version))
5246
+ throw new InvalidDaemonVersionError(version);
5247
+ return version;
5248
+ }
5249
+ function resolveDaemonRuntime(env, defaults) {
5250
+ const cliOverride = env[OMO_LSP_DAEMON_CLI];
5251
+ const versionOverride = env[OMO_LSP_DAEMON_VERSION];
5252
+ const hasCliOverride = cliOverride !== undefined;
5253
+ const hasVersionOverride = versionOverride !== undefined;
5254
+ if (hasCliOverride !== hasVersionOverride) {
5255
+ throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
5256
+ }
5257
+ if (!hasCliOverride || !hasVersionOverride) {
5258
+ if (!isAbsolute3(defaults.cliPath)) {
5259
+ throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
5260
+ }
5261
+ return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
5262
+ }
5263
+ if (!isAbsolute3(cliOverride)) {
5264
+ throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
5265
+ }
5266
+ let cliStats;
3399
5267
  try {
3400
- unlinkSync2(path);
3401
- } catch (error) {}
5268
+ cliStats = statSync4(cliOverride);
5269
+ } catch (error) {
5270
+ if (!(error instanceof Error))
5271
+ throw error;
5272
+ throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
5273
+ }
5274
+ if (!cliStats.isFile()) {
5275
+ throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
5276
+ }
5277
+ return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
5278
+ }
5279
+
5280
+ // src/paths.ts
5281
+ var requireFromHere = createRequire(import.meta.url);
5282
+ var MAX_SOCKET_PATH_LENGTH = 100;
5283
+
5284
+ class InvalidDaemonDirectoryError extends Error {
5285
+ code = "invalid_daemon_directory";
5286
+ directory;
5287
+ constructor(directory) {
5288
+ super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
5289
+ this.name = "InvalidDaemonDirectoryError";
5290
+ this.directory = directory;
5291
+ }
5292
+ }
5293
+ function resolveDaemonVersion(requireFn = requireFromHere) {
5294
+ for (const candidate of ["./package.json", "../package.json"]) {
5295
+ let loaded;
5296
+ try {
5297
+ loaded = requireFn(candidate);
5298
+ } catch (error) {
5299
+ if (!(error instanceof Error))
5300
+ throw error;
5301
+ continue;
5302
+ }
5303
+ if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
5304
+ const version = Reflect.get(loaded, "version");
5305
+ if (typeof version === "string")
5306
+ return validateDaemonVersion(version);
5307
+ }
5308
+ }
5309
+ return "0";
5310
+ }
5311
+ function packagedRuntimeDefaults() {
5312
+ return {
5313
+ cliPath: fileURLToPath3(new URL("./cli.js", import.meta.url)),
5314
+ version: resolveDaemonVersion()
5315
+ };
5316
+ }
5317
+ function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
5318
+ const override = env[OMO_LSP_DAEMON_DIR];
5319
+ if (override !== undefined) {
5320
+ if (!platform.path.isAbsolute(override))
5321
+ throw new InvalidDaemonDirectoryError(override);
5322
+ return platform.path.resolve(override);
5323
+ }
5324
+ return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
5325
+ }
5326
+ function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
5327
+ const runtime = resolveDaemonRuntime(env, runtimeDefaults);
5328
+ const baseDir = daemonBaseDir(env, platform);
5329
+ const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
5330
+ return {
5331
+ version: runtime.version,
5332
+ cliPath: runtime.cliPath,
5333
+ dir,
5334
+ socket: resolveSocketPath(dir, runtime.version, platform),
5335
+ lock: platform.path.join(dir, "daemon.lock"),
5336
+ pid: platform.path.join(dir, "daemon.pid"),
5337
+ auth: platform.path.join(dir, "daemon.auth"),
5338
+ endpoint: platform.path.join(dir, "daemon.endpoint"),
5339
+ owner: platform.path.join(dir, "daemon.owner"),
5340
+ log: platform.path.join(dir, "daemon.log")
5341
+ };
5342
+ }
5343
+ function defaultDaemonPlatform() {
5344
+ return {
5345
+ platform: process.platform,
5346
+ homedir: homedir2,
5347
+ tmpdir,
5348
+ getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
5349
+ username: () => userInfo().username,
5350
+ path
5351
+ };
5352
+ }
5353
+ function resolveSocketPath(dir, version, platform) {
5354
+ const canonicalVersionDir = platform.path.resolve(dir);
5355
+ if (platform.platform === "win32") {
5356
+ const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
5357
+ const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
5358
+ return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
5359
+ }
5360
+ const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
5361
+ if (natural.length < MAX_SOCKET_PATH_LENGTH)
5362
+ return natural;
5363
+ return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
5364
+ }
5365
+ function shortDigest(value) {
5366
+ return createHash2("sha256").update(value).digest("hex").slice(0, 16);
5367
+ }
5368
+
5369
+ // src/socket-jsonrpc.ts
5370
+ function encodeJsonLine(message) {
5371
+ return `${JSON.stringify(message)}
5372
+ `;
5373
+ }
5374
+ function createLineDecoder(onMessage, onParseError) {
5375
+ let buffer = "";
5376
+ return {
5377
+ push(chunk) {
5378
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
5379
+ let index = buffer.indexOf(`
5380
+ `);
5381
+ while (index !== -1) {
5382
+ const raw = buffer.slice(0, index).trim();
5383
+ buffer = buffer.slice(index + 1);
5384
+ if (raw.length > 0) {
5385
+ try {
5386
+ onMessage(JSON.parse(raw));
5387
+ } catch (error) {
5388
+ if (error instanceof Error) {
5389
+ onParseError?.(raw, error);
5390
+ } else {
5391
+ throw error;
5392
+ }
5393
+ }
5394
+ }
5395
+ index = buffer.indexOf(`
5396
+ `);
5397
+ }
5398
+ }
5399
+ };
3402
5400
  }
3403
5401
 
3404
5402
  // src/ensure-daemon.ts
3405
5403
  var PROBE_TIMEOUT_MS = 500;
3406
5404
  var DEFAULT_READY_TIMEOUT_MS = 5000;
3407
5405
  var DEFAULT_POLL_INTERVAL_MS = 100;
3408
- var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
3409
5406
 
3410
5407
  class DaemonUnreachableError extends Error {
3411
5408
  constructor(socketPath) {
@@ -3416,58 +5413,59 @@ class DaemonUnreachableError extends Error {
3416
5413
  async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
3417
5414
  const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
3418
5415
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
3419
- if (await deps.probe(paths.socket))
3420
- return;
3421
- const lock = deps.acquireLock(paths.lock);
3422
- if (!lock) {
3423
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
5416
+ if (await deps.probe(paths))
3424
5417
  return;
3425
- }
3426
- try {
3427
- if (await deps.probe(paths.socket))
3428
- return;
3429
- deps.cleanupStaleSocket(paths.socket);
3430
- deps.spawnDaemon(paths);
3431
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
3432
- } finally {
3433
- lock.release();
3434
- }
5418
+ deps.spawnDaemon(paths);
5419
+ await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs);
3435
5420
  }
3436
- async function waitUntilReachable(socketPath, deps, readyTimeoutMs, pollIntervalMs) {
5421
+ async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs) {
3437
5422
  const deadline = deps.now() + readyTimeoutMs;
3438
5423
  for (;; ) {
3439
- if (await deps.probe(socketPath))
5424
+ if (await deps.probe(paths))
3440
5425
  return;
3441
5426
  if (deps.now() >= deadline)
3442
- throw new DaemonUnreachableError(socketPath);
5427
+ throw new DaemonUnreachableError(paths.socket);
3443
5428
  await deps.sleep(pollIntervalMs);
3444
5429
  }
3445
5430
  }
3446
- function probeSocket(socketPath, timeoutMs = PROBE_TIMEOUT_MS) {
3447
- return new Promise((resolve6) => {
3448
- const socket = connect(socketPath);
3449
- const finish = (ok) => {
5431
+ async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS) {
5432
+ const token = readAuthToken(paths);
5433
+ if (!token)
5434
+ return false;
5435
+ return await pingDaemon(paths, token, timeoutMs) !== null;
5436
+ }
5437
+ function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS) {
5438
+ return new Promise((resolve9) => {
5439
+ const socket = connect(paths.socket);
5440
+ let settled = false;
5441
+ const finish = (value) => {
5442
+ if (settled)
5443
+ return;
5444
+ settled = true;
3450
5445
  socket.destroy();
3451
- resolve6(ok);
5446
+ resolve9(value);
3452
5447
  };
3453
- const timer = setTimeout(() => finish(false), timeoutMs);
5448
+ const timer = setTimeout(() => finish(null), timeoutMs);
3454
5449
  timer.unref?.();
3455
- socket.once("connect", () => {
5450
+ const decoder = createLineDecoder((message) => {
3456
5451
  clearTimeout(timer);
3457
- finish(true);
5452
+ finish(parsePingResponse(message));
5453
+ });
5454
+ socket.once("connect", () => {
5455
+ socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
3458
5456
  });
5457
+ socket.on("data", (chunk) => decoder.push(chunk));
3459
5458
  socket.once("error", () => {
3460
5459
  clearTimeout(timer);
3461
- finish(false);
5460
+ finish(null);
3462
5461
  });
3463
5462
  });
3464
5463
  }
3465
5464
  function spawnDaemonProcess(paths) {
3466
- mkdirSync3(dirname5(paths.log), { recursive: true });
5465
+ mkdirSync3(dirname8(paths.log), { recursive: true });
3467
5466
  const logFd = openSync2(paths.log, "a");
3468
5467
  try {
3469
- const cliPath = resolveDaemonCliPath();
3470
- const child = spawn2(execPath, [cliPath, "daemon"], {
5468
+ const child = spawn2(execPath, [paths.cliPath, "daemon"], {
3471
5469
  detached: true,
3472
5470
  stdio: ["ignore", logFd, logFd]
3473
5471
  });
@@ -3476,156 +5474,133 @@ function spawnDaemonProcess(paths) {
3476
5474
  closeSync2(logFd);
3477
5475
  }
3478
5476
  }
3479
- function resolveDaemonCliPath(env = process.env) {
3480
- const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
3481
- if (override)
3482
- return override;
3483
- return fileURLToPath3(new URL("./cli.js", import.meta.url));
3484
- }
3485
5477
  function defaultEnsureDaemonDeps() {
3486
5478
  return {
3487
- probe: (socketPath) => probeSocket(socketPath),
3488
- acquireLock: (lockPath) => tryAcquireLock(lockPath),
3489
- cleanupStaleSocket: (socketPath) => {
3490
- if (existsSync8(socketPath))
3491
- unlinkQuietly(socketPath);
3492
- },
5479
+ probe: (paths) => probeDaemon(paths),
3493
5480
  spawnDaemon: (paths) => spawnDaemonProcess(paths),
3494
- sleep: (ms) => new Promise((resolve6) => {
3495
- setTimeout(resolve6, ms);
5481
+ sleep: (ms) => new Promise((resolve9) => {
5482
+ setTimeout(resolve9, ms);
3496
5483
  }),
3497
5484
  now: () => Date.now()
3498
5485
  };
3499
5486
  }
3500
-
3501
- // src/paths.ts
3502
- import { createHash } from "node:crypto";
3503
- import { createRequire } from "node:module";
3504
- import { homedir as homedir3, tmpdir } from "node:os";
3505
- import { join as join8 } from "node:path";
3506
- var requireFromHere = createRequire(import.meta.url);
3507
- var MAX_SOCKET_PATH_LENGTH = 100;
3508
- var CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
3509
- function resolveDaemonVersion(requireFn = requireFromHere) {
3510
- for (const candidate of ["./package.json", "../package.json"]) {
3511
- try {
3512
- const pkg = requireFn(candidate);
3513
- if (typeof pkg.version === "string" && pkg.version.length > 0)
3514
- return pkg.version;
3515
- } catch {}
3516
- }
3517
- return "0";
3518
- }
3519
- function daemonBaseDir(env = process.env) {
3520
- const explicit = env["CODEX_LSP_DAEMON_DIR"]?.trim();
3521
- if (explicit)
3522
- return explicit;
3523
- const pluginData = env["PLUGIN_DATA"]?.trim();
3524
- if (pluginData)
3525
- return join8(pluginData, "daemon");
3526
- const codexHome = env["CODEX_HOME"]?.trim();
3527
- const home = codexHome && codexHome.length > 0 ? codexHome : join8(homedir3(), ".codex");
3528
- return join8(home, "codex-lsp", "daemon");
3529
- }
3530
- function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
3531
- const dir = join8(daemonBaseDir(env), `v${version}`);
3532
- return {
3533
- version,
3534
- dir,
3535
- socket: resolveSocketPath(dir, version),
3536
- lock: join8(dir, "daemon.lock"),
3537
- pid: join8(dir, "daemon.pid"),
3538
- log: join8(dir, "daemon.log")
3539
- };
3540
- }
3541
- function resolveDaemonVersionFromEnv(env = process.env) {
3542
- const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
3543
- return version && version.length > 0 ? version : null;
3544
- }
3545
- function resolveSocketPath(dir, version) {
3546
- const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
3547
- if (process.platform === "win32") {
3548
- return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
5487
+ function parsePingResponse(message) {
5488
+ if (!message || typeof message !== "object" || Array.isArray(message))
5489
+ return null;
5490
+ const result = Reflect.get(message, "result");
5491
+ if (!result || typeof result !== "object" || Array.isArray(result))
5492
+ return null;
5493
+ const pid = Reflect.get(result, "pid");
5494
+ const nonce = Reflect.get(result, "nonce");
5495
+ const startedAt = Reflect.get(result, "startedAt");
5496
+ const endpoint = Reflect.get(result, "endpoint");
5497
+ if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
5498
+ return null;
5499
+ if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
5500
+ return null;
5501
+ const path2 = Reflect.get(endpoint, "path");
5502
+ const kind = Reflect.get(endpoint, "kind");
5503
+ if (typeof path2 !== "string")
5504
+ return null;
5505
+ if (kind === "windows")
5506
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
5507
+ if (kind === "missing")
5508
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
5509
+ const dev = Reflect.get(endpoint, "dev");
5510
+ const ino = Reflect.get(endpoint, "ino");
5511
+ if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
5512
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
3549
5513
  }
3550
- const natural = join8(dir, "daemon.sock");
3551
- if (natural.length < MAX_SOCKET_PATH_LENGTH)
3552
- return natural;
3553
- return join8(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
5514
+ return null;
3554
5515
  }
3555
5516
 
3556
5517
  // src/request-routing.ts
3557
5518
  var CONTEXT_KEY = "_context";
5519
+
5520
+ class InvalidDaemonRequestError extends Error {
5521
+ name = "InvalidDaemonRequestError";
5522
+ }
3558
5523
  function extractRequestContext(raw) {
3559
5524
  if (!isPlainRecord(raw) || raw["method"] !== "tools/call")
3560
5525
  return { input: raw, context: undefined };
3561
5526
  const params = raw["params"];
3562
5527
  if (!isPlainRecord(params))
3563
- return { input: raw, context: undefined };
5528
+ throw new InvalidDaemonRequestError("Daemon tools/call params must be an object.");
3564
5529
  const args = params["arguments"];
3565
5530
  if (!isPlainRecord(args))
3566
- return { input: raw, context: undefined };
5531
+ throw new InvalidDaemonRequestError("Daemon tools/call arguments must be an object.");
5532
+ if (!Object.hasOwn(args, CONTEXT_KEY)) {
5533
+ throw new InvalidDaemonRequestError("Daemon tools/call arguments must include _context.");
5534
+ }
3567
5535
  const context = parseContext(args[CONTEXT_KEY]);
3568
- if (!context)
3569
- return { input: raw, context: undefined };
3570
5536
  const cleanedArgs = { ...args };
3571
5537
  delete cleanedArgs[CONTEXT_KEY];
3572
5538
  const cleaned = { ...raw, params: { ...params, arguments: cleanedArgs } };
3573
5539
  return { input: cleaned, context };
3574
5540
  }
3575
- function handleDaemonMessage(raw) {
3576
- const { input, context } = extractRequestContext(raw);
3577
- if (context)
3578
- return runWithRequestContext(context, () => handleLspMcpRequest(input));
3579
- return handleLspMcpRequest(input);
3580
- }
3581
- function parseContext(value) {
3582
- if (!isPlainRecord(value))
3583
- return;
3584
- const context = {};
3585
- const cwd = value["cwd"];
3586
- if (typeof cwd === "string")
3587
- context.cwd = cwd;
3588
- const env = value["env"];
3589
- if (isStringRecord2(env))
3590
- context.env = env;
3591
- return context.cwd === undefined && context.env === undefined ? undefined : context;
5541
+ function handleDaemonMessage(raw, state) {
5542
+ const authenticated = authenticateMessage(raw, state.token);
5543
+ if ("error" in authenticated)
5544
+ return Promise.resolve(authenticated);
5545
+ if (authenticated.method === "omo/ping") {
5546
+ return Promise.resolve({
5547
+ jsonrpc: "2.0",
5548
+ id: authenticated.id,
5549
+ result: { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, ...state.owner }
5550
+ });
5551
+ }
5552
+ if (authenticated.method === "$/cancelRequest") {
5553
+ const targetId = cancellationTargetId(authenticated.input);
5554
+ if (targetId !== undefined)
5555
+ state.activeRequests?.get(String(targetId))?.abort();
5556
+ return Promise.resolve(undefined);
5557
+ }
5558
+ let routed;
5559
+ try {
5560
+ routed = extractRequestContext(authenticated.input);
5561
+ } catch (error) {
5562
+ const message = error instanceof Error ? error.message : "invalid daemon request";
5563
+ return Promise.resolve({
5564
+ jsonrpc: "2.0",
5565
+ id: authenticated.id,
5566
+ error: { code: -32602, message, data: { code: "invalid_daemon_request" } }
5567
+ });
5568
+ }
5569
+ const { input, context } = routed;
5570
+ const key = routeRequestKey(authenticated.id);
5571
+ if (key === undefined || !state.activeRequests) {
5572
+ if (context)
5573
+ return runWithRequestContext(context, () => handleLspMcpRequest(input));
5574
+ return handleLspMcpRequest(input);
5575
+ }
5576
+ const controller = new AbortController;
5577
+ state.activeRequests.set(key, controller);
5578
+ const options = { signal: controller.signal };
5579
+ const run = context ? runWithRequestContext(context, () => handleLspMcpRequest(input, options)) : handleLspMcpRequest(input, options);
5580
+ return run.finally(() => {
5581
+ if (state.activeRequests?.get(key) === controller)
5582
+ state.activeRequests.delete(key);
5583
+ });
3592
5584
  }
3593
- function isStringRecord2(value) {
3594
- return isPlainRecord(value) && Object.values(value).every((item) => typeof item === "string");
5585
+ function routeRequestKey(id) {
5586
+ return typeof id === "string" || typeof id === "number" ? String(id) : undefined;
3595
5587
  }
3596
-
3597
- // src/socket-jsonrpc.ts
3598
- function encodeJsonLine(message) {
3599
- return `${JSON.stringify(message)}
3600
- `;
5588
+ function cancellationTargetId(input) {
5589
+ const params = input["params"];
5590
+ if (!isPlainRecord(params))
5591
+ return;
5592
+ const id = params["id"];
5593
+ return typeof id === "string" || typeof id === "number" ? id : undefined;
3601
5594
  }
3602
- function createLineDecoder(onMessage, onParseError) {
3603
- let buffer = "";
3604
- return {
3605
- push(chunk) {
3606
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
3607
- let index = buffer.indexOf(`
3608
- `);
3609
- while (index !== -1) {
3610
- const raw = buffer.slice(0, index).trim();
3611
- buffer = buffer.slice(index + 1);
3612
- if (raw.length > 0) {
3613
- try {
3614
- onMessage(JSON.parse(raw));
3615
- } catch (error) {
3616
- onParseError?.(raw, error);
3617
- }
3618
- }
3619
- index = buffer.indexOf(`
3620
- `);
3621
- }
3622
- }
3623
- };
5595
+ function parseContext(value) {
5596
+ if (!isPlainRecord(value))
5597
+ throw new InvalidDaemonRequestError("LSP request _context must be an object.");
5598
+ return parseLspRequestContext(value);
3624
5599
  }
3625
5600
 
3626
5601
  // src/daemon-client.ts
3627
5602
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
3628
- var REQUEST_ID = 1;
5603
+ var nextProxyRequestId = 1;
3629
5604
 
3630
5605
  class DaemonRequestError extends Error {
3631
5606
  requestWritten;
@@ -3635,44 +5610,70 @@ class DaemonRequestError extends Error {
3635
5610
  this.requestWritten = requestWritten;
3636
5611
  }
3637
5612
  }
3638
- async function callToolViaDaemon(name, args, options = {}) {
5613
+
5614
+ class DaemonAuthenticationRejectedError extends DaemonRequestError {
5615
+ constructor() {
5616
+ super("daemon authentication failed before dispatch", true);
5617
+ this.name = "DaemonAuthenticationRejectedError";
5618
+ }
5619
+ }
5620
+
5621
+ class DaemonRequestCancelledError extends DaemonRequestError {
5622
+ constructor(requestWritten) {
5623
+ super("daemon request cancelled", requestWritten);
5624
+ this.name = "DaemonRequestCancelledError";
5625
+ }
5626
+ }
5627
+ async function callToolViaDaemon(name, args, options) {
5628
+ const context = requireContext(options.context);
3639
5629
  const paths = options.paths ?? daemonPaths();
3640
5630
  const ensure = options.ensure ?? ensureDaemonRunning;
3641
5631
  const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
3642
- const requestArgs = withContext(args, options.context);
5632
+ const requestArgs = withContext(args, context);
3643
5633
  let lastError;
3644
- for (let attempt = 0;attempt < 2; attempt += 1) {
5634
+ let authRefreshUsed = false;
5635
+ for (let attempt = 0;attempt < 3; attempt += 1) {
3645
5636
  try {
3646
5637
  await ensure(paths);
3647
- return await sendToolCall(paths.socket, name, requestArgs, timeoutMs);
5638
+ const token = readAuthToken(paths);
5639
+ if (!token)
5640
+ throw new DaemonRequestError("daemon auth token missing", false);
5641
+ const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
5642
+ return await sendToolCall(paths, token, name, requestArgs, sendOptions);
3648
5643
  } catch (error) {
3649
5644
  lastError = error;
3650
- if (error instanceof DaemonRequestError && error.requestWritten)
5645
+ if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
5646
+ authRefreshUsed = true;
5647
+ continue;
5648
+ }
5649
+ if (error instanceof DaemonRequestCancelledError)
5650
+ break;
5651
+ if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
3651
5652
  break;
3652
5653
  }
3653
5654
  }
3654
5655
  return daemonUnreachableResult(paths, lastError);
3655
5656
  }
3656
- function callDiagnosticsViaDaemon(filePath, options = {}) {
5657
+ function callDiagnosticsViaDaemon(filePath, options) {
3657
5658
  return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
3658
5659
  }
3659
- var FORWARDED_ENV_KEYS = [
3660
- "LSP_TOOLS_MCP_PROJECT_CONFIG",
3661
- "LSP_TOOLS_MCP_USER_CONFIG",
3662
- "LSP_TOOLS_MCP_INSTALL_DECISIONS"
3663
- ];
3664
5660
  function currentRequestContext(env = process.env) {
3665
- const forwarded = {};
3666
- for (const key of FORWARDED_ENV_KEYS) {
3667
- const value = env[key];
3668
- if (value !== undefined)
3669
- forwarded[key] = value;
3670
- }
3671
- return { cwd: process.cwd(), env: forwarded };
5661
+ const cwd = process.cwd();
5662
+ const home = env["HOME"] ?? homedir3();
5663
+ return parseLspRequestContext({
5664
+ cwd,
5665
+ projectConfigPaths: [join7(cwd, ".codex", "lsp-client.json")],
5666
+ userConfigPath: join7(home, ".codex", "lsp-client.json"),
5667
+ installDecisionsPath: join7(home, ".codex", "lsp-install-decisions.json"),
5668
+ capabilities: { installDecisionTool: true }
5669
+ });
5670
+ }
5671
+ function requireContext(context) {
5672
+ if (!context)
5673
+ throw new DaemonRequestError("daemon tool context is required", false);
5674
+ return parseLspRequestContext(context);
3672
5675
  }
3673
5676
  function withContext(args, context) {
3674
- if (!context || context.cwd === undefined && context.env === undefined)
3675
- return args;
3676
5677
  return { ...args, [CONTEXT_KEY]: context };
3677
5678
  }
3678
5679
  function daemonUnreachableResult(paths, error) {
@@ -3686,39 +5687,84 @@ function daemonUnreachableResult(paths, error) {
3686
5687
  `);
3687
5688
  return { content: [{ type: "text", text: text2 }], isError: true };
3688
5689
  }
3689
- function sendToolCall(socketPath, name, args, timeoutMs) {
3690
- return new Promise((resolve6, reject) => {
3691
- const socket = connect2(socketPath);
5690
+ function sendToolCall(paths, token, name, args, options) {
5691
+ return new Promise((resolve9, reject) => {
5692
+ const socket = connect2(paths.socket);
5693
+ const requestId = allocateProxyRequestId();
3692
5694
  let settled = false;
3693
5695
  let requestWritten = false;
5696
+ let cancelAfterWrite = false;
5697
+ const cancelPayload = () => encodeJsonLine({
5698
+ jsonrpc: "2.0",
5699
+ method: "$/cancelRequest",
5700
+ params: { _omo: authEnvelope(token), id: requestId }
5701
+ });
3694
5702
  const finish = (run) => {
3695
5703
  if (settled)
3696
5704
  return;
3697
5705
  settled = true;
3698
5706
  clearTimeout(timer);
3699
- socket.destroy();
5707
+ options.signal?.removeEventListener("abort", onAbort);
5708
+ destroyAfterCancel();
3700
5709
  run();
3701
5710
  };
3702
- const timer = setTimeout(() => finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten))), timeoutMs);
5711
+ const sendCancel = () => {
5712
+ if (!requestWritten) {
5713
+ cancelAfterWrite = true;
5714
+ return;
5715
+ }
5716
+ if (!socket.writable)
5717
+ return;
5718
+ socket.write(cancelPayload());
5719
+ };
5720
+ const destroyAfterCancel = () => {
5721
+ socket.destroy();
5722
+ };
5723
+ const onAbort = () => {
5724
+ sendCancel();
5725
+ finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
5726
+ };
5727
+ const timer = setTimeout(() => {
5728
+ sendCancel();
5729
+ finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
5730
+ }, options.timeoutMs);
3703
5731
  timer.unref();
5732
+ if (options.signal?.aborted) {
5733
+ onAbort();
5734
+ return;
5735
+ }
5736
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3704
5737
  const decoder = createLineDecoder((message) => {
3705
- const result = toToolResult(message);
5738
+ if (isAuthErrorResponse(message)) {
5739
+ finish(() => reject(new DaemonAuthenticationRejectedError));
5740
+ return;
5741
+ }
5742
+ const result = toToolResult(message, requestId);
3706
5743
  if (result)
3707
- finish(() => resolve6(result));
5744
+ finish(() => resolve9(result));
3708
5745
  else
3709
5746
  finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
3710
5747
  });
3711
5748
  socket.once("connect", () => {
3712
- requestWritten = true;
3713
- socket.write(encodeJsonLine({ jsonrpc: "2.0", id: REQUEST_ID, method: "tools/call", params: { name, arguments: args } }));
5749
+ const payload = encodeJsonLine({
5750
+ jsonrpc: "2.0",
5751
+ id: requestId,
5752
+ method: "tools/call",
5753
+ params: { _omo: authEnvelope(token), name, arguments: args }
5754
+ });
5755
+ socket.write(payload, () => {
5756
+ requestWritten = true;
5757
+ if (cancelAfterWrite && socket.writable)
5758
+ socket.write(cancelPayload());
5759
+ });
3714
5760
  });
3715
5761
  socket.on("data", (chunk) => decoder.push(chunk));
3716
5762
  socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
3717
5763
  socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
3718
5764
  });
3719
5765
  }
3720
- function toToolResult(message) {
3721
- if (!isPlainRecord(message) || message["id"] !== REQUEST_ID)
5766
+ function toToolResult(message, requestId) {
5767
+ if (!isPlainRecord(message) || message["id"] !== requestId)
3722
5768
  return null;
3723
5769
  const result = message["result"];
3724
5770
  if (!isPlainRecord(result) || !Array.isArray(result["content"]))
@@ -3729,6 +5775,16 @@ function toToolResult(message) {
3729
5775
  details: result["details"]
3730
5776
  };
3731
5777
  }
5778
+ function allocateProxyRequestId() {
5779
+ const id = nextProxyRequestId;
5780
+ nextProxyRequestId += 1;
5781
+ if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
5782
+ nextProxyRequestId = 1;
5783
+ return id;
5784
+ }
5785
+ function isRetryableTool(name) {
5786
+ return name !== "rename" && name !== "lsp_rename";
5787
+ }
3732
5788
  function errorText(error) {
3733
5789
  return error instanceof Error ? error.message : String(error);
3734
5790
  }
@@ -3738,13 +5794,21 @@ async function runMcpStdioProxy(options = {}) {
3738
5794
  const input = options.input ?? process.stdin;
3739
5795
  const output = options.output ?? process.stdout;
3740
5796
  const paths = options.paths ?? daemonPaths();
3741
- const context = options.context ?? currentRequestContext();
5797
+ const env = options.env ?? process.env;
5798
+ const cwd = options.cwd ?? inferOpenCodeProjectCwd(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]);
5799
+ const contextEnv = cwd === undefined ? env : canonicalizeContextEnv(env);
5800
+ const contextInput = {
5801
+ env: contextEnv,
5802
+ ...cwd === undefined ? {} : { cwd },
5803
+ ...options.homeDir === undefined ? {} : { homeDir: options.homeDir }
5804
+ };
5805
+ const context = options.context ?? createStandaloneMcpRequestContext(contextInput);
3742
5806
  const callOptions = { paths, context, ...options.ensure ? { ensure: options.ensure } : {} };
3743
5807
  await runJsonRpcStdioServer({
3744
5808
  input,
3745
5809
  output,
3746
5810
  idleTimeoutMs: 0,
3747
- handler: handleProxyRequest,
5811
+ handler: (request, requestOptions) => runWithRequestContext(context, () => handleProxyRequest(request, requestOptions)),
3748
5812
  handlerOptions: callOptions,
3749
5813
  onHandlerError: (error) => {
3750
5814
  process.stderr.write(`[lsp-daemon] proxy error: ${error instanceof Error ? error.message : String(error)}
@@ -3768,43 +5832,332 @@ function asToolCall(parsed) {
3768
5832
  const args = params["arguments"];
3769
5833
  return { id: jsonRpcId(parsed["id"]), name: params["name"], args: isPlainRecord(args) ? args : {} };
3770
5834
  }
5835
+ function inferOpenCodeProjectCwd(projectConfigEnv) {
5836
+ if (!projectConfigEnv)
5837
+ return;
5838
+ for (const entry of projectConfigEnv.split(delimiter4)) {
5839
+ const projectRoot = projectRootFromOpenCodeConfigPath(entry);
5840
+ if (projectRoot)
5841
+ return projectRoot;
5842
+ }
5843
+ return;
5844
+ }
5845
+ function canonicalizeContextEnv(env) {
5846
+ return {
5847
+ ...env,
5848
+ LSP_TOOLS_MCP_PROJECT_CONFIG: canonicalizePathList(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]),
5849
+ LSP_TOOLS_MCP_USER_CONFIG: canonicalizePath(env["LSP_TOOLS_MCP_USER_CONFIG"]),
5850
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: canonicalizePath(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"])
5851
+ };
5852
+ }
5853
+ function canonicalizePathList(value) {
5854
+ if (value === undefined)
5855
+ return;
5856
+ return value.split(delimiter4).map((entry) => canonicalizePath(entry) ?? entry).join(delimiter4);
5857
+ }
5858
+ function canonicalizePath(value) {
5859
+ if (value === undefined || !isAbsolute4(value) || !existsSync11(value))
5860
+ return value;
5861
+ return realpathSync4(value);
5862
+ }
5863
+ function projectRootFromOpenCodeConfigPath(path2) {
5864
+ if (basename3(path2) !== "lsp.json" && basename3(path2) !== "lsp-client.json")
5865
+ return;
5866
+ const configDir = dirname9(path2);
5867
+ const configDirName = basename3(configDir);
5868
+ if (configDirName !== ".opencode" && configDirName !== ".omo")
5869
+ return;
5870
+ return dirname9(configDir);
5871
+ }
3771
5872
 
3772
5873
  // src/daemon-server.ts
3773
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
5874
+ import { chmodSync as chmodSync2 } from "node:fs";
3774
5875
  import { createServer as createServer2 } from "node:net";
3775
- import { join as join9 } from "node:path";
5876
+
5877
+ // src/ownership.ts
5878
+ import { randomUUID } from "node:crypto";
5879
+ import { existsSync as existsSync12, lstatSync as lstatSync7, readFileSync as readFileSync7, statSync as statSync5, unlinkSync as unlinkSync3 } from "node:fs";
5880
+ import { dirname as dirname11 } from "node:path";
5881
+
5882
+ // src/lock.ts
5883
+ import { closeSync as closeSync3, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeSync as writeSync2 } from "node:fs";
5884
+ import { dirname as dirname10 } from "node:path";
5885
+ function isProcessAlive(pid) {
5886
+ if (!Number.isInteger(pid) || pid <= 0)
5887
+ return false;
5888
+ try {
5889
+ process.kill(pid, 0);
5890
+ return true;
5891
+ } catch (error) {
5892
+ if (error instanceof Error)
5893
+ return errorCode3(error) === "EPERM";
5894
+ throw error;
5895
+ }
5896
+ }
5897
+ function readLockPid(lockPath) {
5898
+ try {
5899
+ const pid = Number.parseInt(readFileSync6(lockPath, "utf8").trim(), 10);
5900
+ return Number.isInteger(pid) ? pid : null;
5901
+ } catch {
5902
+ return null;
5903
+ }
5904
+ }
5905
+ function tryAcquireLock(lockPath, ownerPid = process.pid) {
5906
+ mkdirSync4(dirname10(lockPath), { recursive: true });
5907
+ for (let attempt = 0;attempt < 2; attempt += 1) {
5908
+ const handle = writeLockFile(lockPath, ownerPid);
5909
+ if (handle)
5910
+ return handle;
5911
+ if (!reapStaleLock(lockPath))
5912
+ return null;
5913
+ }
5914
+ return null;
5915
+ }
5916
+ function writeLockFile(lockPath, ownerPid) {
5917
+ try {
5918
+ const fd = openSync3(lockPath, "wx", 384);
5919
+ writeSync2(fd, `${ownerPid}
5920
+ `);
5921
+ closeSync3(fd);
5922
+ return { release: () => unlinkQuietly(lockPath) };
5923
+ } catch (error) {
5924
+ if (errorCode3(error) === "EEXIST")
5925
+ return null;
5926
+ throw error;
5927
+ }
5928
+ }
5929
+ function reapStaleLock(lockPath) {
5930
+ const pid = readLockPid(lockPath);
5931
+ if (pid !== null && isProcessAlive(pid))
5932
+ return false;
5933
+ unlinkQuietly(lockPath);
5934
+ return true;
5935
+ }
5936
+ function unlinkQuietly(path2) {
5937
+ try {
5938
+ unlinkSync2(path2);
5939
+ } catch (error) {
5940
+ if (error instanceof Error)
5941
+ return;
5942
+ throw error;
5943
+ }
5944
+ }
5945
+ function errorCode3(error) {
5946
+ if (!error || typeof error !== "object" || !("code" in error))
5947
+ return;
5948
+ const code = Reflect.get(error, "code");
5949
+ return typeof code === "string" ? code : undefined;
5950
+ }
5951
+
5952
+ // src/ownership.ts
5953
+ class DaemonAlreadyRunningError extends Error {
5954
+ name = "DaemonAlreadyRunningError";
5955
+ code = "daemon_already_running";
5956
+ }
5957
+
5958
+ class DaemonStartupDeferredError extends Error {
5959
+ reason;
5960
+ name = "DaemonStartupDeferredError";
5961
+ code = "daemon_startup_deferred";
5962
+ constructor(reason) {
5963
+ super(`LSP daemon startup deferred: ${reason}`);
5964
+ this.reason = reason;
5965
+ }
5966
+ }
5967
+ async function acquireStartupLease(paths, pingOwner) {
5968
+ ensureDaemonDirectories(paths);
5969
+ const lock = tryAcquireLock(paths.lock);
5970
+ if (!lock) {
5971
+ const token = readAuthToken(paths);
5972
+ if (token && await pingOwner(token))
5973
+ throw new DaemonAlreadyRunningError("LSP daemon already running");
5974
+ throw new DaemonStartupDeferredError("startup_lock_busy");
5975
+ }
5976
+ try {
5977
+ const token = await validateExistingOwner(paths, pingOwner);
5978
+ return { lock, token, owner: newOwner(paths) };
5979
+ } catch (error) {
5980
+ lock.release();
5981
+ throw error;
5982
+ }
5983
+ }
5984
+ function ensureDaemonDirectories(paths) {
5985
+ ensurePrivateDirectory(paths.dir);
5986
+ if (process.platform !== "win32")
5987
+ ensurePrivateDirectory(dirname11(paths.socket));
5988
+ }
5989
+ function readDaemonOwner(paths) {
5990
+ try {
5991
+ return parseOwner(JSON.parse(readFileSync7(paths.owner, "utf8")));
5992
+ } catch (error) {
5993
+ if (error instanceof Error)
5994
+ return null;
5995
+ throw error;
5996
+ }
5997
+ }
5998
+ function writeDaemonOwner(paths, owner) {
5999
+ writePrivateFile(paths.pid, `${owner.pid}
6000
+ `);
6001
+ writePrivateFile(paths.endpoint, owner.endpoint.path);
6002
+ writePrivateFile(paths.owner, `${JSON.stringify(owner)}
6003
+ `);
6004
+ }
6005
+ function removeDaemonMetadataForOwner(paths, owner) {
6006
+ const current = readDaemonOwner(paths);
6007
+ if (!current || !sameOwner(current, owner))
6008
+ return;
6009
+ unlinkQuietly(paths.socket);
6010
+ unlinkQuietly(paths.pid);
6011
+ unlinkQuietly(paths.endpoint);
6012
+ unlinkQuietly(paths.owner);
6013
+ }
6014
+ function endpointIdentity(endpointPath) {
6015
+ if (process.platform === "win32")
6016
+ return { kind: "windows", path: endpointPath };
6017
+ try {
6018
+ const stats = statSync5(endpointPath);
6019
+ return { kind: "unix", path: endpointPath, dev: stats.dev, ino: stats.ino };
6020
+ } catch (error) {
6021
+ if (error instanceof Error)
6022
+ return { kind: "missing", path: endpointPath };
6023
+ throw error;
6024
+ }
6025
+ }
6026
+ function sameEndpoint(a, b) {
6027
+ if (a.kind !== b.kind || a.path !== b.path)
6028
+ return false;
6029
+ switch (a.kind) {
6030
+ case "unix":
6031
+ return b.kind === "unix" && a.dev === b.dev && a.ino === b.ino;
6032
+ case "windows":
6033
+ return true;
6034
+ case "missing":
6035
+ return true;
6036
+ }
6037
+ }
6038
+ function sameOwner(a, b) {
6039
+ return a.pid === b.pid && a.nonce === b.nonce && sameEndpoint(a.endpoint, b.endpoint);
6040
+ }
6041
+ async function validateExistingOwner(paths, pingOwner) {
6042
+ let token = readOrCreateAuthToken(paths);
6043
+ for (let attempt = 0;attempt < 2; attempt += 1) {
6044
+ const owner = readDaemonOwner(paths);
6045
+ if (!owner)
6046
+ return token;
6047
+ const ping = await pingOwner(token);
6048
+ if (ping && owner.nonce === ping.nonce && sameEndpoint(owner.endpoint, ping.endpoint)) {
6049
+ throw new DaemonAlreadyRunningError("LSP daemon already running");
6050
+ }
6051
+ if (ping)
6052
+ continue;
6053
+ if (isProcessAlive(owner.pid))
6054
+ throw new DaemonStartupDeferredError("owner_pid_live_unreachable");
6055
+ const reread = readDaemonOwner(paths);
6056
+ const endpoint = endpointIdentity(owner.endpoint.path);
6057
+ if (!reread || reread.nonce !== owner.nonce || !sameEndpoint(endpoint, owner.endpoint)) {
6058
+ throw new DaemonStartupDeferredError("owner_changed_during_cleanup");
6059
+ }
6060
+ cleanupDeadOwner(paths, owner);
6061
+ token = rotateAuthToken(paths);
6062
+ return token;
6063
+ }
6064
+ throw new DaemonStartupDeferredError("reachable_owner_mismatch");
6065
+ }
6066
+ function cleanupDeadOwner(paths, owner) {
6067
+ if (process.platform !== "win32" && existsSync12(owner.endpoint.path)) {
6068
+ const stat = lstatSync7(owner.endpoint.path);
6069
+ if (stat.isSocket())
6070
+ unlinkSync3(owner.endpoint.path);
6071
+ }
6072
+ unlinkQuietly(paths.pid);
6073
+ unlinkQuietly(paths.endpoint);
6074
+ unlinkQuietly(paths.owner);
6075
+ }
6076
+ function newOwner(paths) {
6077
+ return {
6078
+ pid: process.pid,
6079
+ nonce: randomUUID(),
6080
+ startedAt: new Date().toISOString(),
6081
+ endpoint: endpointIdentity(paths.socket)
6082
+ };
6083
+ }
6084
+ function parseOwner(value) {
6085
+ if (!value || typeof value !== "object" || Array.isArray(value))
6086
+ return null;
6087
+ const pid = Reflect.get(value, "pid");
6088
+ const nonce = Reflect.get(value, "nonce");
6089
+ const startedAt = Reflect.get(value, "startedAt");
6090
+ const endpoint = parseEndpoint(Reflect.get(value, "endpoint"));
6091
+ if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string" || !endpoint)
6092
+ return null;
6093
+ return { pid, nonce, startedAt, endpoint };
6094
+ }
6095
+ function parseEndpoint(value) {
6096
+ if (!value || typeof value !== "object" || Array.isArray(value))
6097
+ return null;
6098
+ const kind = Reflect.get(value, "kind");
6099
+ const path2 = Reflect.get(value, "path");
6100
+ if (typeof path2 !== "string")
6101
+ return null;
6102
+ if (kind === undefined)
6103
+ return endpointIdentity(path2);
6104
+ if (kind === "windows")
6105
+ return { kind, path: path2 };
6106
+ if (kind === "missing")
6107
+ return { kind, path: path2 };
6108
+ const dev = Reflect.get(value, "dev");
6109
+ const ino = Reflect.get(value, "ino");
6110
+ if (kind === "unix" && typeof dev === "number" && typeof ino === "number")
6111
+ return { kind, path: path2, dev, ino };
6112
+ return null;
6113
+ }
6114
+
6115
+ // src/daemon-server.ts
3776
6116
  var DEFAULT_IDLE_SHUTDOWN_MS = 30 * 60000;
3777
6117
  var DEFAULT_IDLE_CHECK_INTERVAL_MS = 60000;
3778
6118
  async function startDaemonServer(paths, options = {}) {
3779
6119
  const idleShutdownMs = options.idleShutdownMs ?? DEFAULT_IDLE_SHUTDOWN_MS;
3780
6120
  const idleCheckIntervalMs = options.idleCheckIntervalMs ?? DEFAULT_IDLE_CHECK_INTERVAL_MS;
3781
- mkdirSync4(paths.dir, { recursive: true });
3782
- unlinkQuietly(paths.socket);
6121
+ const lease = await acquireStartupLease(paths, (token) => pingDaemon(paths, token));
3783
6122
  const connections = new Set;
3784
6123
  let lastActiveAt = Date.now();
3785
6124
  const touch = () => {
3786
6125
  lastActiveAt = Date.now();
3787
6126
  };
6127
+ let routeOwner = lease.owner;
3788
6128
  const server2 = createServer2((socket) => {
3789
6129
  connections.add(socket);
6130
+ const activeRequests = new Map;
3790
6131
  touch();
3791
6132
  const decoder = createLineDecoder((message) => {
3792
6133
  touch();
3793
- respond(socket, message);
6134
+ respond(socket, message, lease.token, routeOwner, activeRequests);
3794
6135
  });
3795
6136
  socket.on("data", (chunk) => decoder.push(chunk));
3796
6137
  socket.on("error", () => socket.destroy());
3797
6138
  socket.on("close", () => {
6139
+ for (const controller of activeRequests.values())
6140
+ controller.abort();
6141
+ activeRequests.clear();
3798
6142
  connections.delete(socket);
3799
6143
  touch();
3800
6144
  });
3801
6145
  });
3802
6146
  server2.on("error", (error) => logServerError(error));
3803
- const endpointPath = join9(paths.dir, "daemon.endpoint");
3804
- await listen(server2, paths.socket);
3805
- writeFileSync3(paths.pid, `${process.pid}
3806
- `);
3807
- writeFileSync3(endpointPath, paths.socket);
6147
+ let owner;
6148
+ try {
6149
+ await listen(server2, paths.socket);
6150
+ if (process.platform !== "win32")
6151
+ chmodSync2(paths.socket, 384);
6152
+ owner = { ...lease.owner, endpoint: endpointIdentity(paths.socket) };
6153
+ routeOwner = owner;
6154
+ writeDaemonOwner(paths, owner);
6155
+ await assertSelfProbe(paths, lease.token, owner);
6156
+ } catch (error) {
6157
+ lease.lock.release();
6158
+ throw error;
6159
+ }
6160
+ lease.lock.release();
3808
6161
  let closed = false;
3809
6162
  const close = async () => {
3810
6163
  if (closed)
@@ -3815,9 +6168,7 @@ async function startDaemonServer(paths, options = {}) {
3815
6168
  socket.destroy();
3816
6169
  connections.clear();
3817
6170
  await closeServer(server2);
3818
- unlinkQuietly(paths.socket);
3819
- unlinkQuietly(paths.pid);
3820
- unlinkQuietly(endpointPath);
6171
+ removeDaemonMetadataForOwner(paths, owner);
3821
6172
  await disposeDefaultLspManager();
3822
6173
  };
3823
6174
  const idleTimer = setInterval(() => {
@@ -3839,27 +6190,37 @@ async function startDaemonServer(paths, options = {}) {
3839
6190
  installSignalHandlers(close);
3840
6191
  return { server: server2, close };
3841
6192
  }
3842
- async function respond(socket, message) {
6193
+ async function respond(socket, message, token, owner, activeRequests) {
3843
6194
  try {
3844
- const response = await handleDaemonMessage(message);
6195
+ const response = await handleDaemonMessage(message, { token, owner, activeRequests });
3845
6196
  if (response && socket.writable)
3846
6197
  socket.write(encodeJsonLine(response));
3847
6198
  } catch (error) {
6199
+ if (!(error instanceof Error))
6200
+ throw error;
3848
6201
  logServerError(error);
3849
6202
  }
3850
6203
  }
6204
+ async function assertSelfProbe(paths, token, owner) {
6205
+ setPrivateFileMode(paths.pid);
6206
+ setPrivateFileMode(paths.endpoint);
6207
+ setPrivateFileMode(paths.owner);
6208
+ const ping = await pingDaemon(paths, token);
6209
+ if (!ping || ping.nonce !== owner.nonce)
6210
+ throw new DaemonStartupDeferredError("self_probe_failed");
6211
+ }
3851
6212
  function listen(server2, socketPath) {
3852
- return new Promise((resolve6, reject) => {
6213
+ return new Promise((resolve9, reject) => {
3853
6214
  const onError = (error) => reject(error);
3854
6215
  server2.once("error", onError);
3855
6216
  server2.listen(socketPath, () => {
3856
6217
  server2.removeListener("error", onError);
3857
- resolve6();
6218
+ resolve9();
3858
6219
  });
3859
6220
  });
3860
6221
  }
3861
6222
  function closeServer(server2) {
3862
- return new Promise((resolve6) => server2.close(() => resolve6()));
6223
+ return new Promise((resolve9) => server2.close(() => resolve9()));
3863
6224
  }
3864
6225
  function installSignalHandlers(close) {
3865
6226
  const handler = () => {
@@ -3878,7 +6239,13 @@ function logServerError(error) {
3878
6239
  async function runDaemon() {
3879
6240
  process.on("uncaughtException", (error) => logNonFatal("uncaughtException", error));
3880
6241
  process.on("unhandledRejection", (reason) => logNonFatal("unhandledRejection", reason));
3881
- await startDaemonServer(daemonPaths());
6242
+ try {
6243
+ await startDaemonServer(daemonPaths());
6244
+ } catch (error) {
6245
+ if (error instanceof DaemonAlreadyRunningError)
6246
+ return;
6247
+ throw error;
6248
+ }
3882
6249
  }
3883
6250
  function logNonFatal(kind, error) {
3884
6251
  const message = error instanceof Error ? error.stack ?? error.message : String(error);