oh-my-opencode 4.18.0 → 4.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/dist/cli/index.js +457 -154
  8. package/dist/cli-node/index.js +457 -154
  9. package/dist/index.js +415 -388
  10. package/package.json +16 -16
  11. package/packages/lsp-core/package.json +4 -0
  12. package/packages/lsp-core/src/index.ts +1 -0
  13. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  14. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  15. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  16. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  17. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  18. package/packages/lsp-core/src/lsp/client.ts +262 -80
  19. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  20. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  21. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
  22. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
  23. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  24. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  25. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  26. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  27. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  28. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  29. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  30. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  31. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  32. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  33. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  34. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  35. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  36. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  37. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  38. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  39. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  40. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  41. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  42. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  43. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  44. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  45. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  46. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  47. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  48. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  60. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  61. package/packages/lsp-core/src/mcp.ts +18 -7
  62. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  63. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  64. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  65. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  66. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  67. package/packages/lsp-core/src/request-context.test.ts +171 -0
  68. package/packages/lsp-core/src/request-context.ts +222 -9
  69. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  70. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  71. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  72. package/packages/lsp-core/src/tools/rename.ts +10 -15
  73. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  74. package/packages/lsp-core/src/tools/types.ts +2 -1
  75. package/packages/lsp-daemon/dist/cli.js +3114 -747
  76. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  77. package/packages/lsp-daemon/dist/client.js +5851 -0
  78. package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
  79. package/packages/lsp-daemon/dist/daemon-client.js +113 -30
  80. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  81. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  82. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
  83. package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
  84. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  85. package/packages/lsp-daemon/dist/index.js +2862 -754
  86. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  87. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  88. package/packages/lsp-daemon/dist/lock.js +14 -4
  89. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  90. package/packages/lsp-daemon/dist/ownership.js +168 -0
  91. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  92. package/packages/lsp-daemon/dist/paths.js +72 -33
  93. package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
  94. package/packages/lsp-daemon/dist/proxy.js +54 -3
  95. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  96. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  97. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  98. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  99. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  100. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  101. package/packages/lsp-daemon/package.json +12 -3
  102. package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
  103. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  104. package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
  105. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  106. package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
  107. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  108. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  109. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  110. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  111. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  112. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  113. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  114. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  115. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  116. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  117. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  118. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
  119. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  120. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  122. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  123. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  124. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  125. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  126. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  127. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  128. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  129. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  130. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  131. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  132. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  133. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  134. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  135. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  136. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  137. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  138. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
  139. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  140. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
  141. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  142. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  143. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  144. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  145. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  146. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  147. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  148. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  149. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  150. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  151. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  153. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  155. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  156. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  165. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  179. package/packages/omo-codex/plugin/package-lock.json +26 -14
  180. package/packages/omo-codex/plugin/package.json +1 -1
  181. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  182. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  183. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  184. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
  185. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  186. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  187. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  188. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  189. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  190. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  191. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  192. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -1,80 +1,542 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // components/lsp/src/cli.ts
3
+ // src/cli.ts
4
4
  import { spawn as spawn3 } from "node:child_process";
5
5
  import { argv, execPath as execPath2, stderr } from "node:process";
6
6
 
7
- // components/lsp/src/codex-hook-cli.ts
7
+ // src/codex-hook-cli.ts
8
8
  import { stdin as processStdin } from "node:process";
9
9
 
10
- // ../../lsp-daemon/dist/index.js
11
- import { readFileSync } from "node:fs";
12
- import { resolve } from "node:path";
13
- import { pathToFileURL as pathToFileURL2 } from "node:url";
14
- import { AsyncLocalStorage } from "node:async_hooks";
15
- import { pathToFileURL } from "node:url";
16
- import { spawn, spawnSync } from "node:child_process";
17
- import { existsSync, statSync } from "node:fs";
18
- import { delimiter, join } from "node:path";
19
- import { basename, extname } from "node:path";
10
+ // src/codex-hook.ts
11
+ import { readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
12
+ import { homedir as homedir3 } from "node:os";
13
+ import { join as join11, resolve as resolve11 } from "node:path";
14
+
15
+ // ../../../../lsp-daemon/dist/client.js
20
16
  import { connect as connect2 } from "node:net";
21
- import { spawn as spawn2 } from "node:child_process";
22
- import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
17
+ import { AsyncLocalStorage } from "node:async_hooks";
18
+ import { existsSync, realpathSync, statSync } from "node:fs";
19
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
20
+ import { spawn } from "node:child_process";
21
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
23
22
  import { connect } from "node:net";
24
- import { dirname as dirname2 } from "node:path";
23
+ import { dirname as dirname3 } from "node:path";
25
24
  import { execPath } from "node:process";
26
- import { fileURLToPath } from "node:url";
27
- import { closeSync, mkdirSync, openSync, readFileSync as readFileSync2, unlinkSync, writeSync } from "node:fs";
28
- import { dirname } from "node:path";
25
+ import {
26
+ chmodSync,
27
+ closeSync,
28
+ constants,
29
+ fchmodSync,
30
+ fstatSync,
31
+ lstatSync,
32
+ mkdirSync,
33
+ openSync,
34
+ readFileSync,
35
+ unlinkSync,
36
+ writeSync
37
+ } from "node:fs";
29
38
  import { createHash } from "node:crypto";
30
39
  import { createRequire } from "node:module";
31
- import { homedir, tmpdir } from "node:os";
32
- import { join as join2 } from "node:path";
33
- import { resolve as resolve4 } from "node:path";
34
- import { existsSync as existsSync6, statSync as statSync2 } from "node:fs";
35
- import { dirname as dirname4, join as join6, resolve as resolve2 } from "node:path";
36
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
37
- import { homedir as homedir2 } from "node:os";
38
- import { dirname as dirname3, isAbsolute, join as join3 } from "node:path";
39
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
40
- import { homedir as homedir3 } from "node:os";
41
- import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join4 } from "node:path";
42
- import { existsSync as existsSync5 } from "node:fs";
43
- import { delimiter as delimiter3, join as join5 } from "node:path";
44
- import { existsSync as existsSync7, lstatSync, readdirSync } from "node:fs";
45
- import { join as join7, resolve as resolve3 } from "node:path";
40
+ import { homedir as homedir2, tmpdir, userInfo } from "node:os";
41
+ import * as path from "node:path";
42
+ import { fileURLToPath } from "node:url";
43
+ import { statSync as statSync2 } from "node:fs";
44
+ import { isAbsolute as isAbsolute2 } from "node:path";
45
+ import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
46
+ import { dirname as dirname8, join as join4, resolve as resolve7 } from "node:path";
47
+ import { basename as basename2, extname } from "node:path";
48
+ import { resolve as resolve6 } from "node:path";
49
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
50
+ import { pathToFileURL } from "node:url";
51
+ import { spawn as spawn2, spawnSync } from "node:child_process";
52
+ import { existsSync as existsSync2, statSync as statSync3 } from "node:fs";
53
+ import { delimiter as delimiter2, join as join2 } from "node:path";
54
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
55
+ import { relative as relative2, resolve as resolve2 } from "node:path";
56
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
57
+ import { existsSync as existsSync4, lstatSync as lstatSync3, renameSync, rmSync, writeFileSync } from "node:fs";
58
+ import { existsSync as existsSync3, lstatSync as lstatSync2, readFileSync as readFileSync3, readdirSync, realpathSync as realpathSync3 } from "node:fs";
59
+ import { dirname as dirname4, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
46
60
  import { fileURLToPath as fileURLToPath2 } from "node:url";
47
- import { lstatSync as lstatSync2, readdirSync as readdirSync2 } from "node:fs";
48
- import { join as join8 } from "node:path";
49
- import { existsSync as existsSync8, readFileSync as readFileSync5, realpathSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
50
- import { dirname as dirname5, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
61
+ import { createHash as createHash2 } from "node:crypto";
62
+ import { dirname as dirname5, relative as relative4, resolve as resolve4 } from "node:path";
63
+ import { existsSync as existsSync5, lstatSync as lstatSync4, readdirSync as readdirSync2 } from "node:fs";
64
+ import { dirname as dirname6, resolve as resolve5 } from "node:path";
65
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
66
+ import { dirname as dirname7 } from "node:path";
67
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
68
+ import { existsSync as existsSync8 } from "node:fs";
69
+ import { delimiter as delimiter3, join as join3 } from "node:path";
70
+ import { existsSync as existsSync10, lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
71
+ import { join as join5, resolve as resolve8 } from "node:path";
51
72
  import { fileURLToPath as fileURLToPath3 } from "node:url";
52
- function reportBestEffortCleanupError(operation, error) {
53
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1")
54
- return;
55
- const message = error instanceof Error ? error.message : String(error);
56
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
73
+ import { lstatSync as lstatSync6, readdirSync as readdirSync4 } from "node:fs";
74
+ import { join as join6 } from "node:path";
75
+ import { statSync as statSync5 } from "node:fs";
76
+ import { isAbsolute as isAbsolute4 } from "node:path";
77
+
78
+ class LspRequestContextParseError extends Error {
79
+ code;
80
+ name = "LspRequestContextParseError";
81
+ constructor(code, message) {
82
+ super(message);
83
+ this.code = code;
84
+ }
85
+ }
86
+
87
+ class LspRequestContextUnavailableError extends Error {
88
+ name = "LspRequestContextUnavailableError";
89
+ constructor() {
90
+ super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
91
+ }
57
92
  }
58
93
  var storage = new AsyncLocalStorage;
94
+ var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
95
+ var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
96
+ function lspRequestContext() {
97
+ const context = storage.getStore();
98
+ if (!context)
99
+ throw new LspRequestContextUnavailableError;
100
+ return context;
101
+ }
59
102
  function contextCwd() {
60
- return storage.getStore()?.cwd ?? process.cwd();
103
+ return lspRequestContext().cwd;
104
+ }
105
+ function parseLspRequestContext(value) {
106
+ if (!isRecord(value)) {
107
+ throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
108
+ }
109
+ rejectUnknownFields(value, CONTEXT_FIELDS, "context");
110
+ const cwd = stringField(value, "cwd");
111
+ const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
112
+ const userConfigPath = stringField(value, "userConfigPath");
113
+ const installDecisionsPath = stringField(value, "installDecisionsPath");
114
+ const capabilities = capabilitiesField(value["capabilities"]);
115
+ const canonical = canonicalCwd(cwd);
116
+ for (const path2 of projectConfigPaths) {
117
+ requireAbsolutePath(path2, "projectConfigPaths");
118
+ const projectPath = canonicalizeExistingOrNearestAncestor(path2);
119
+ if (!isPathInside(canonical, projectPath)) {
120
+ throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path2}`);
121
+ }
122
+ }
123
+ requireAbsolutePath(userConfigPath, "userConfigPath");
124
+ requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
125
+ return {
126
+ cwd: canonical,
127
+ projectConfigPaths: projectConfigPaths.map((path2) => canonicalizeExistingOrNearestAncestor(path2)),
128
+ userConfigPath,
129
+ installDecisionsPath,
130
+ capabilities
131
+ };
61
132
  }
62
- function contextEnv(key) {
63
- const store = storage.getStore();
64
- if (store?.env)
65
- return store.env[key];
66
- return process.env[key];
133
+ function canonicalCwd(cwd) {
134
+ const resolved = resolve(cwd);
135
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
136
+ throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
137
+ }
138
+ return realpathSync(resolved);
139
+ }
140
+ function canonicalizeExistingOrNearestAncestor(path2) {
141
+ let current = resolve(path2);
142
+ const suffix = [];
143
+ while (true) {
144
+ try {
145
+ const existing = realpathSync(current);
146
+ return suffix.length === 0 ? existing : join(existing, ...suffix);
147
+ } catch (error) {
148
+ if (!isMissingPathError(error))
149
+ throw error;
150
+ const parent = dirname(current);
151
+ if (parent === current)
152
+ throw error;
153
+ suffix.unshift(basename(current));
154
+ current = parent;
155
+ }
156
+ }
157
+ }
158
+ function capabilitiesField(value) {
159
+ if (!isRecord(value)) {
160
+ throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
161
+ }
162
+ rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
163
+ const installDecisionTool = value["installDecisionTool"];
164
+ if (typeof installDecisionTool !== "boolean") {
165
+ throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
166
+ }
167
+ return { installDecisionTool };
168
+ }
169
+ function stringField(value, field) {
170
+ const fieldValue = value[field];
171
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
172
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
173
+ }
174
+ return fieldValue;
175
+ }
176
+ function stringArrayField(value, field) {
177
+ const fieldValue = value[field];
178
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
179
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
180
+ }
181
+ return fieldValue;
182
+ }
183
+ function requireAbsolutePath(path2, field) {
184
+ if (!isAbsolute(path2)) {
185
+ throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path2}`);
186
+ }
187
+ }
188
+ function isPathInside(parent, child) {
189
+ const childPath = resolve(child);
190
+ const relativePath = relative(parent, childPath);
191
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
192
+ }
193
+ function isMissingPathError(error) {
194
+ const code = errorCode(error);
195
+ return code === "ENOENT" || code === "ENOTDIR";
196
+ }
197
+ function rejectUnknownFields(value, allowed, scope) {
198
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
199
+ if (unknown.length > 0) {
200
+ throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
201
+ }
202
+ }
203
+ function isRecord(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+ function errorCode(error) {
207
+ if (!error || typeof error !== "object" || !("code" in error))
208
+ return;
209
+ const code = Reflect.get(error, "code");
210
+ return typeof code === "string" ? code : undefined;
211
+ }
212
+ function isPlainRecord(value) {
213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
214
+ }
215
+ var OMO_DAEMON_PROTOCOL_VERSION = 1;
216
+ var AUTH_ERROR_CODE = -32001;
217
+ function authEnvelope(token) {
218
+ return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
219
+ }
220
+ function readAuthToken(paths) {
221
+ try {
222
+ const token = readFileSync(paths.auth, "utf8").trim();
223
+ return token.length > 0 ? token : null;
224
+ } catch (error) {
225
+ if (error instanceof Error)
226
+ return null;
227
+ throw error;
228
+ }
229
+ }
230
+ function isAuthErrorResponse(message) {
231
+ if (!isPlainRecord(message))
232
+ return false;
233
+ const error = message["error"];
234
+ if (!isPlainRecord(error))
235
+ return false;
236
+ const data = error["data"];
237
+ return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
238
+ }
239
+ var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
240
+ var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
241
+ var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
242
+ var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
243
+
244
+ class InvalidRuntimeOverrideError extends Error {
245
+ code = "invalid_runtime_override";
246
+ reason;
247
+ constructor(reason, message) {
248
+ super(message);
249
+ this.name = "InvalidRuntimeOverrideError";
250
+ this.reason = reason;
251
+ }
252
+ }
253
+
254
+ class InvalidDaemonVersionError extends Error {
255
+ code = "invalid_daemon_version";
256
+ version;
257
+ constructor(version) {
258
+ super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
259
+ this.name = "InvalidDaemonVersionError";
260
+ this.version = version;
261
+ }
262
+ }
263
+ function validateDaemonVersion(version) {
264
+ if (!DAEMON_VERSION_PATTERN.test(version))
265
+ throw new InvalidDaemonVersionError(version);
266
+ return version;
267
+ }
268
+ function resolveDaemonRuntime(env, defaults) {
269
+ const cliOverride = env[OMO_LSP_DAEMON_CLI];
270
+ const versionOverride = env[OMO_LSP_DAEMON_VERSION];
271
+ const hasCliOverride = cliOverride !== undefined;
272
+ const hasVersionOverride = versionOverride !== undefined;
273
+ if (hasCliOverride !== hasVersionOverride) {
274
+ throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
275
+ }
276
+ if (!hasCliOverride || !hasVersionOverride) {
277
+ if (!isAbsolute2(defaults.cliPath)) {
278
+ throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
279
+ }
280
+ return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
281
+ }
282
+ if (!isAbsolute2(cliOverride)) {
283
+ throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
284
+ }
285
+ let cliStats;
286
+ try {
287
+ cliStats = statSync2(cliOverride);
288
+ } catch (error) {
289
+ if (!(error instanceof Error))
290
+ throw error;
291
+ throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
292
+ }
293
+ if (!cliStats.isFile()) {
294
+ throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
295
+ }
296
+ return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
297
+ }
298
+ var requireFromHere = createRequire(import.meta.url);
299
+ var MAX_SOCKET_PATH_LENGTH = 100;
300
+
301
+ class InvalidDaemonDirectoryError extends Error {
302
+ code = "invalid_daemon_directory";
303
+ directory;
304
+ constructor(directory) {
305
+ super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
306
+ this.name = "InvalidDaemonDirectoryError";
307
+ this.directory = directory;
308
+ }
309
+ }
310
+ function resolveDaemonVersion(requireFn = requireFromHere) {
311
+ for (const candidate of ["./package.json", "../package.json"]) {
312
+ let loaded;
313
+ try {
314
+ loaded = requireFn(candidate);
315
+ } catch (error) {
316
+ if (!(error instanceof Error))
317
+ throw error;
318
+ continue;
319
+ }
320
+ if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
321
+ const version = Reflect.get(loaded, "version");
322
+ if (typeof version === "string")
323
+ return validateDaemonVersion(version);
324
+ }
325
+ }
326
+ return "0";
327
+ }
328
+ function packagedRuntimeDefaults() {
329
+ return {
330
+ cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
331
+ version: resolveDaemonVersion()
332
+ };
333
+ }
334
+ function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
335
+ const override = env[OMO_LSP_DAEMON_DIR];
336
+ if (override !== undefined) {
337
+ if (!platform.path.isAbsolute(override))
338
+ throw new InvalidDaemonDirectoryError(override);
339
+ return platform.path.resolve(override);
340
+ }
341
+ return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
342
+ }
343
+ function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
344
+ const runtime = resolveDaemonRuntime(env, runtimeDefaults);
345
+ const baseDir = daemonBaseDir(env, platform);
346
+ const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
347
+ return {
348
+ version: runtime.version,
349
+ cliPath: runtime.cliPath,
350
+ dir,
351
+ socket: resolveSocketPath(dir, runtime.version, platform),
352
+ lock: platform.path.join(dir, "daemon.lock"),
353
+ pid: platform.path.join(dir, "daemon.pid"),
354
+ auth: platform.path.join(dir, "daemon.auth"),
355
+ endpoint: platform.path.join(dir, "daemon.endpoint"),
356
+ owner: platform.path.join(dir, "daemon.owner"),
357
+ log: platform.path.join(dir, "daemon.log")
358
+ };
359
+ }
360
+ function defaultDaemonPlatform() {
361
+ return {
362
+ platform: process.platform,
363
+ homedir: homedir2,
364
+ tmpdir,
365
+ getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
366
+ username: () => userInfo().username,
367
+ path
368
+ };
369
+ }
370
+ function resolveSocketPath(dir, version, platform) {
371
+ const canonicalVersionDir = platform.path.resolve(dir);
372
+ if (platform.platform === "win32") {
373
+ const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
374
+ const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
375
+ return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
376
+ }
377
+ const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
378
+ if (natural.length < MAX_SOCKET_PATH_LENGTH)
379
+ return natural;
380
+ return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
381
+ }
382
+ function shortDigest(value) {
383
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
384
+ }
385
+ function encodeJsonLine(message) {
386
+ return `${JSON.stringify(message)}
387
+ `;
388
+ }
389
+ function createLineDecoder(onMessage, onParseError) {
390
+ let buffer = "";
391
+ return {
392
+ push(chunk) {
393
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
394
+ let index = buffer.indexOf(`
395
+ `);
396
+ while (index !== -1) {
397
+ const raw = buffer.slice(0, index).trim();
398
+ buffer = buffer.slice(index + 1);
399
+ if (raw.length > 0) {
400
+ try {
401
+ onMessage(JSON.parse(raw));
402
+ } catch (error) {
403
+ if (error instanceof Error) {
404
+ onParseError?.(raw, error);
405
+ } else {
406
+ throw error;
407
+ }
408
+ }
409
+ }
410
+ index = buffer.indexOf(`
411
+ `);
412
+ }
413
+ }
414
+ };
415
+ }
416
+ var PROBE_TIMEOUT_MS = 500;
417
+ var DEFAULT_READY_TIMEOUT_MS = 5000;
418
+ var DEFAULT_POLL_INTERVAL_MS = 100;
419
+
420
+ class DaemonUnreachableError extends Error {
421
+ constructor(socketPath) {
422
+ super(`LSP daemon did not become reachable at ${socketPath}`);
423
+ this.name = "DaemonUnreachableError";
424
+ }
425
+ }
426
+ async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
427
+ const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
428
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
429
+ if (await deps.probe(paths))
430
+ return;
431
+ deps.spawnDaemon(paths);
432
+ await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs);
433
+ }
434
+ async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs) {
435
+ const deadline = deps.now() + readyTimeoutMs;
436
+ for (;; ) {
437
+ if (await deps.probe(paths))
438
+ return;
439
+ if (deps.now() >= deadline)
440
+ throw new DaemonUnreachableError(paths.socket);
441
+ await deps.sleep(pollIntervalMs);
442
+ }
443
+ }
444
+ async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS) {
445
+ const token = readAuthToken(paths);
446
+ if (!token)
447
+ return false;
448
+ return await pingDaemon(paths, token, timeoutMs) !== null;
449
+ }
450
+ function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS) {
451
+ return new Promise((resolve22) => {
452
+ const socket = connect(paths.socket);
453
+ let settled = false;
454
+ const finish = (value) => {
455
+ if (settled)
456
+ return;
457
+ settled = true;
458
+ socket.destroy();
459
+ resolve22(value);
460
+ };
461
+ const timer = setTimeout(() => finish(null), timeoutMs);
462
+ timer.unref?.();
463
+ const decoder = createLineDecoder((message) => {
464
+ clearTimeout(timer);
465
+ finish(parsePingResponse(message));
466
+ });
467
+ socket.once("connect", () => {
468
+ socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
469
+ });
470
+ socket.on("data", (chunk) => decoder.push(chunk));
471
+ socket.once("error", () => {
472
+ clearTimeout(timer);
473
+ finish(null);
474
+ });
475
+ });
476
+ }
477
+ function spawnDaemonProcess(paths) {
478
+ mkdirSync2(dirname3(paths.log), { recursive: true });
479
+ const logFd = openSync2(paths.log, "a");
480
+ try {
481
+ const child = spawn(execPath, [paths.cliPath, "daemon"], {
482
+ detached: true,
483
+ stdio: ["ignore", logFd, logFd]
484
+ });
485
+ child.unref();
486
+ } finally {
487
+ closeSync2(logFd);
488
+ }
489
+ }
490
+ function defaultEnsureDaemonDeps() {
491
+ return {
492
+ probe: (paths) => probeDaemon(paths),
493
+ spawnDaemon: (paths) => spawnDaemonProcess(paths),
494
+ sleep: (ms) => new Promise((resolve22) => {
495
+ setTimeout(resolve22, ms);
496
+ }),
497
+ now: () => Date.now()
498
+ };
499
+ }
500
+ function parsePingResponse(message) {
501
+ if (!message || typeof message !== "object" || Array.isArray(message))
502
+ return null;
503
+ const result = Reflect.get(message, "result");
504
+ if (!result || typeof result !== "object" || Array.isArray(result))
505
+ return null;
506
+ const pid = Reflect.get(result, "pid");
507
+ const nonce = Reflect.get(result, "nonce");
508
+ const startedAt = Reflect.get(result, "startedAt");
509
+ const endpoint = Reflect.get(result, "endpoint");
510
+ if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
511
+ return null;
512
+ if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
513
+ return null;
514
+ const path2 = Reflect.get(endpoint, "path");
515
+ const kind = Reflect.get(endpoint, "kind");
516
+ if (typeof path2 !== "string")
517
+ return null;
518
+ if (kind === "windows")
519
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
520
+ if (kind === "missing")
521
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
522
+ const dev = Reflect.get(endpoint, "dev");
523
+ const ino = Reflect.get(endpoint, "ino");
524
+ if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
525
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
526
+ }
527
+ return null;
528
+ }
529
+ var HEADER_SEPARATOR = Buffer.from(`\r
530
+ \r
531
+ `);
532
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60000;
533
+ var BASENAME_EXTENSIONS = {
534
+ Dockerfile: ".dockerfile",
535
+ Containerfile: ".dockerfile"
536
+ };
537
+ function effectiveExtension(filePath) {
538
+ return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
67
539
  }
68
- var DEFAULT_MAX_REFERENCES = 200;
69
- var DEFAULT_MAX_SYMBOLS = 200;
70
- var DEFAULT_MAX_DIAGNOSTICS = 200;
71
- var DEFAULT_MAX_DIRECTORY_FILES = 50;
72
- var REQUEST_TIMEOUT_MS = 15000;
73
- var INIT_TIMEOUT_MS = 60000;
74
- var IDLE_TIMEOUT_MS = 5 * 60000;
75
- var REAPER_INTERVAL_MS = 60000;
76
- var STOP_HARD_KILL_TIMEOUT_MS = 5000;
77
- var STOP_SIGKILL_GRACE_MS = 1000;
78
540
 
79
541
  class LspConnectionClosedError extends Error {
80
542
  serverId;
@@ -122,7 +584,12 @@ class LspInvalidPathError extends Error {
122
584
  }
123
585
 
124
586
  class LspServerLookupError extends Error {
587
+ lookup;
125
588
  name = "LspServerLookupError";
589
+ constructor(message, lookup) {
590
+ super(message);
591
+ this.lookup = lookup;
592
+ }
126
593
  }
127
594
 
128
595
  class LspServerInitializingError extends Error {
@@ -140,7 +607,25 @@ class LspProcessSpawnError extends Error {
140
607
  function isLspDeadConnectionError(err) {
141
608
  return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
142
609
  }
143
- var HEADER_SEPARATOR = `\r
610
+ function writeCleanupError(message) {
611
+ process.stderr.write(`${message}
612
+ `);
613
+ }
614
+ function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
615
+ const message = error instanceof Error ? error.message : String(error);
616
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
617
+ }
618
+ var DEFAULT_MAX_REFERENCES = 200;
619
+ var DEFAULT_MAX_SYMBOLS = 200;
620
+ var DEFAULT_MAX_DIAGNOSTICS = 200;
621
+ var DEFAULT_MAX_DIRECTORY_FILES = 50;
622
+ var REQUEST_TIMEOUT_MS = 15000;
623
+ var INIT_TIMEOUT_MS = 60000;
624
+ var IDLE_TIMEOUT_MS = 5 * 60000;
625
+ var REAPER_INTERVAL_MS = 60000;
626
+ var STOP_HARD_KILL_TIMEOUT_MS = 5000;
627
+ var STOP_SIGKILL_GRACE_MS = 1000;
628
+ var HEADER_SEPARATOR2 = `\r
144
629
  \r
145
630
  `;
146
631
  var PARSE_ERROR = -32700;
@@ -186,28 +671,80 @@ class JsonRpcConnection {
186
671
  onError(handler) {
187
672
  this.errorHandlers.push(handler);
188
673
  }
189
- async sendRequest(method, params) {
674
+ async sendRequest(method, params, options = {}) {
190
675
  if (this.disposed)
191
676
  throw new Error("JSON-RPC connection is disposed");
192
677
  const id = this.nextRequestId;
193
678
  this.nextRequestId += 1;
679
+ const key = String(id);
194
680
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
195
- const responsePromise = new Promise((resolve6, reject) => {
196
- this.pendingRequests.set(String(id), {
681
+ let requestWritten = false;
682
+ let cancelAfterWrite = false;
683
+ let settled = false;
684
+ const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
685
+ const responsePromise = new Promise((resolve22, reject) => {
686
+ const cleanup = () => {
687
+ options.signal?.removeEventListener("abort", onAbort);
688
+ };
689
+ const settleCancel = () => {
690
+ if (settled)
691
+ return;
692
+ settled = true;
693
+ this.pendingRequests.delete(key);
694
+ cleanup();
695
+ const rejectCancelled = () => reject(abortError(options.signal));
696
+ if (!requestWritten) {
697
+ cancelAfterWrite = true;
698
+ rejectCancelled();
699
+ return;
700
+ }
701
+ writeCancel().then(rejectCancelled, (error) => {
702
+ this.emitError(toError(error));
703
+ rejectCancelled();
704
+ });
705
+ };
706
+ const onAbort = () => settleCancel();
707
+ this.pendingRequests.set(key, {
197
708
  resolve(result) {
198
- resolve6(result);
709
+ settled = true;
710
+ cleanup();
711
+ resolve22(result);
199
712
  },
200
- reject
713
+ reject(error) {
714
+ settled = true;
715
+ cleanup();
716
+ reject(error);
717
+ },
718
+ cleanup
201
719
  });
720
+ if (options.signal?.aborted) {
721
+ settleCancel();
722
+ return;
723
+ }
724
+ options.signal?.addEventListener("abort", onAbort, { once: true });
202
725
  });
726
+ if (settled)
727
+ return responsePromise;
203
728
  try {
204
729
  await this.writeMessage(message);
730
+ requestWritten = true;
731
+ if (cancelAfterWrite)
732
+ await writeCancel();
205
733
  } catch (error) {
206
- this.pendingRequests.delete(String(id));
734
+ if (settled)
735
+ return responsePromise;
736
+ const pending = this.pendingRequests.get(key);
737
+ if (pending) {
738
+ pending.cleanup();
739
+ this.pendingRequests.delete(key);
740
+ }
207
741
  throw error;
208
742
  }
209
743
  return responsePromise;
210
744
  }
745
+ pendingRequestCount() {
746
+ return this.pendingRequests.size;
747
+ }
211
748
  async sendNotification(method, params) {
212
749
  if (this.disposed)
213
750
  return;
@@ -224,6 +761,7 @@ class JsonRpcConnection {
224
761
  this.reader.off("error", this.handleStreamError);
225
762
  this.writer.off("error", this.handleStreamError);
226
763
  for (const pending of this.pendingRequests.values()) {
764
+ pending.cleanup();
227
765
  pending.reject(new Error("JSON-RPC connection disposed"));
228
766
  }
229
767
  this.pendingRequests.clear();
@@ -245,17 +783,17 @@ class JsonRpcConnection {
245
783
  };
246
784
  drainInputBuffer() {
247
785
  while (true) {
248
- const headerEnd = this.inputBuffer.indexOf(HEADER_SEPARATOR);
786
+ const headerEnd = this.inputBuffer.indexOf(HEADER_SEPARATOR2);
249
787
  if (headerEnd === -1)
250
788
  return;
251
789
  const headers = this.inputBuffer.subarray(0, headerEnd).toString("ascii");
252
- const contentLength = parseContentLength(headers);
790
+ const contentLength = parseContentLength2(headers);
253
791
  if (contentLength === null) {
254
792
  this.inputBuffer = Buffer.alloc(0);
255
793
  this.emitError(new Error("JSON-RPC message is missing Content-Length header"));
256
794
  return;
257
795
  }
258
- const bodyStart = headerEnd + Buffer.byteLength(HEADER_SEPARATOR);
796
+ const bodyStart = headerEnd + Buffer.byteLength(HEADER_SEPARATOR2);
259
797
  const bodyEnd = bodyStart + contentLength;
260
798
  if (this.inputBuffer.length < bodyEnd)
261
799
  return;
@@ -299,6 +837,7 @@ class JsonRpcConnection {
299
837
  if (!pending)
300
838
  return;
301
839
  this.pendingRequests.delete(String(id));
840
+ pending.cleanup();
302
841
  if ("error" in message) {
303
842
  pending.reject(jsonRpcErrorToError(message["error"]));
304
843
  return;
@@ -312,7 +851,11 @@ class JsonRpcConnection {
312
851
  try {
313
852
  handler(params);
314
853
  } catch (error) {
315
- this.emitError(toError(error));
854
+ if (error instanceof Error) {
855
+ this.emitError(error);
856
+ return;
857
+ }
858
+ this.emitError(new Error(String(error)));
316
859
  }
317
860
  }
318
861
  handleRequest(message) {
@@ -337,13 +880,13 @@ class JsonRpcConnection {
337
880
  const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
338
881
  \r
339
882
  ${body}`;
340
- return new Promise((resolve6, reject) => {
883
+ return new Promise((resolve22, reject) => {
341
884
  this.writer.write(payload, (error) => {
342
885
  if (error) {
343
886
  reject(error);
344
887
  return;
345
888
  }
346
- resolve6();
889
+ resolve22();
347
890
  });
348
891
  });
349
892
  }
@@ -353,7 +896,15 @@ ${body}`;
353
896
  }
354
897
  }
355
898
  }
356
- function parseContentLength(headers) {
899
+ function abortError(signal) {
900
+ const reason = signal?.reason;
901
+ if (reason instanceof Error)
902
+ return reason;
903
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
904
+ error.name = "AbortError";
905
+ return error;
906
+ }
907
+ function parseContentLength2(headers) {
357
908
  for (const line of headers.split(`\r
358
909
  `)) {
359
910
  const separatorIndex = line.indexOf(":");
@@ -401,10 +952,10 @@ function reportKillError(context, error) {
401
952
  }
402
953
  function validateCwd(cwd) {
403
954
  try {
404
- if (!existsSync(cwd)) {
955
+ if (!existsSync2(cwd)) {
405
956
  return { valid: false, error: `Working directory does not exist: ${cwd}` };
406
957
  }
407
- const stats = statSync(cwd);
958
+ const stats = statSync3(cwd);
408
959
  if (!stats.isDirectory()) {
409
960
  return { valid: false, error: `Path is not a directory: ${cwd}` };
410
961
  }
@@ -417,9 +968,9 @@ function validateCwd(cwd) {
417
968
  }
418
969
  }
419
970
  function wrap(proc) {
420
- const exitedPromise = new Promise((resolve6) => {
421
- proc.once("close", (code) => resolve6(code ?? 0));
422
- proc.once("error", () => resolve6(1));
971
+ const exitedPromise = new Promise((resolve22) => {
972
+ proc.once("close", (code) => resolve22(code ?? 0));
973
+ proc.once("error", () => resolve22(1));
423
974
  });
424
975
  if (!proc.stdin || !proc.stdout || !proc.stderr) {
425
976
  throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
@@ -473,7 +1024,7 @@ function isWindowsShellShim(command) {
473
1024
  return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
474
1025
  }
475
1026
  function splitPath(pathValue, platform) {
476
- const separator = platform === "win32" ? ";" : delimiter;
1027
+ const separator = platform === "win32" ? ";" : delimiter2;
477
1028
  return pathValue.split(separator).filter(Boolean);
478
1029
  }
479
1030
  function getWindowsPathExtensions(env) {
@@ -488,8 +1039,8 @@ function resolveWindowsCommand(command, env) {
488
1039
  const extensions = getWindowsPathExtensions(env);
489
1040
  for (const baseDirectory of baseDirectories) {
490
1041
  for (const extension of extensions) {
491
- const candidate = baseDirectory ? join(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
492
- if (existsSync(candidate))
1042
+ const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
1043
+ if (existsSync2(candidate))
493
1044
  return candidate;
494
1045
  }
495
1046
  }
@@ -523,7 +1074,7 @@ function spawnProcess(command, options) {
523
1074
  throw new LspProcessSpawnError("[lsp] empty command");
524
1075
  }
525
1076
  const preparedCommand = createSpawnCommand(command, process.platform, process.env["ComSpec"] ?? "cmd.exe", options.env);
526
- const proc = spawn(preparedCommand.command, preparedCommand.args, {
1077
+ const proc = spawn2(preparedCommand.command, preparedCommand.args, {
527
1078
  cwd: options.cwd,
528
1079
  env: options.env,
529
1080
  stdio: ["pipe", "pipe", "pipe"],
@@ -533,15 +1084,15 @@ function spawnProcess(command, options) {
533
1084
  });
534
1085
  return wrap(proc);
535
1086
  }
536
- function isRecord(value) {
1087
+ function isRecord2(value) {
537
1088
  return typeof value === "object" && value !== null && !Array.isArray(value);
538
1089
  }
539
1090
  function parseConfigurationItems(params) {
540
- if (!isRecord(params) || !Array.isArray(params["items"]))
1091
+ if (!isRecord2(params) || !Array.isArray(params["items"]))
541
1092
  return [];
542
1093
  const items = [];
543
1094
  for (const item of params["items"]) {
544
- if (!isRecord(item))
1095
+ if (!isRecord2(item))
545
1096
  continue;
546
1097
  const section = item["section"];
547
1098
  items.push(section === undefined || typeof section !== "string" ? {} : { section });
@@ -549,10 +1100,34 @@ function parseConfigurationItems(params) {
549
1100
  return items;
550
1101
  }
551
1102
  function parseDiagnosticsParams(params) {
552
- if (!isRecord(params) || typeof params["uri"] !== "string")
1103
+ if (!isRecord2(params) || typeof params["uri"] !== "string")
553
1104
  return null;
554
1105
  const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
555
- return { uri: params["uri"], diagnostics };
1106
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
1107
+ return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
1108
+ }
1109
+ function createLspSpawnEnv(_root, input) {
1110
+ return { ...input };
1111
+ }
1112
+ function isDiagnostic(value) {
1113
+ return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
1114
+ }
1115
+ function isRange(value) {
1116
+ return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
1117
+ }
1118
+ function isPosition(value) {
1119
+ return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
1120
+ }
1121
+
1122
+ class LspClientNotStartedError extends Error {
1123
+ serverId;
1124
+ root;
1125
+ name = "LspClientNotStartedError";
1126
+ constructor(serverId, root) {
1127
+ super("LSP client not started");
1128
+ this.serverId = serverId;
1129
+ this.root = root;
1130
+ }
556
1131
  }
557
1132
 
558
1133
  class LspClientTransport {
@@ -565,9 +1140,11 @@ class LspClientTransport {
565
1140
  diagnosticsStore = new Map;
566
1141
  requestTimeoutMs;
567
1142
  initializeTimeoutMs;
568
- constructor(root, server, timeouts = {}) {
1143
+ workspaceApplyEditHandler = null;
1144
+ diagnosticPullSupported = false;
1145
+ constructor(root, server2, timeouts = {}) {
569
1146
  this.root = root;
570
- this.server = server;
1147
+ this.server = server2;
571
1148
  this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
572
1149
  this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
573
1150
  }
@@ -577,6 +1154,21 @@ class LspClientTransport {
577
1154
  command() {
578
1155
  return [...this.server.command];
579
1156
  }
1157
+ setWorkspaceApplyEditHandler(handler) {
1158
+ this.workspaceApplyEditHandler = handler;
1159
+ }
1160
+ hasWorkspaceApplyEditHandler() {
1161
+ return this.workspaceApplyEditHandler !== null;
1162
+ }
1163
+ setDiagnosticPullSupported(supported) {
1164
+ this.diagnosticPullSupported = supported;
1165
+ }
1166
+ isDiagnosticPullSupported() {
1167
+ return this.diagnosticPullSupported;
1168
+ }
1169
+ handlePublishDiagnostics(params) {
1170
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
1171
+ }
580
1172
  async start() {
581
1173
  const env = createLspSpawnEnv(this.root, {
582
1174
  ...process.env,
@@ -587,7 +1179,6 @@ class LspClientTransport {
587
1179
  env
588
1180
  });
589
1181
  this.startStderrReading();
590
- await new Promise((resolve6) => setTimeout(resolve6, 100));
591
1182
  if (this.proc.exitCode !== null) {
592
1183
  const stderr = this.stderrBuffer.join(`
593
1184
  `);
@@ -597,7 +1188,7 @@ class LspClientTransport {
597
1188
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
598
1189
  const diagnosticsParams = parseDiagnosticsParams(params);
599
1190
  if (diagnosticsParams?.uri) {
600
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
1191
+ this.handlePublishDiagnostics(diagnosticsParams);
601
1192
  }
602
1193
  });
603
1194
  this.connection.onRequest("workspace/configuration", (params) => {
@@ -610,6 +1201,9 @@ class LspClientTransport {
610
1201
  });
611
1202
  this.connection.onRequest("client/registerCapability", () => null);
612
1203
  this.connection.onRequest("window/workDoneProgress/create", () => null);
1204
+ if (this.workspaceApplyEditHandler) {
1205
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
1206
+ }
613
1207
  this.connection.onClose(() => {
614
1208
  this.processExited = true;
615
1209
  });
@@ -638,30 +1232,25 @@ class LspClientTransport {
638
1232
  }
639
1233
  async sendRequest(method, ...args) {
640
1234
  if (!this.connection)
641
- throw new Error("LSP client not started");
1235
+ throw new LspClientNotStartedError(this.server.id, this.root);
642
1236
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
643
1237
  const stderrTail = this.stderrBuffer.slice(-10).join(`
644
1238
  `);
645
1239
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
646
1240
  }
647
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
648
- let timeoutHandle = null;
649
- const timeoutPromise = new Promise((_, reject) => {
650
- timeoutHandle = setTimeout(() => {
651
- const stderrTail = this.stderrBuffer.slice(-5).join(`
1241
+ const options = args[1];
1242
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
1243
+ const timeoutController = new AbortController;
1244
+ const timeoutHandle = setTimeout(() => {
1245
+ const stderrTail = this.stderrBuffer.slice(-5).join(`
652
1246
  `);
653
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
654
- }, timeoutMs);
655
- });
1247
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
1248
+ }, timeoutMs);
1249
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
656
1250
  try {
657
- const requestPromise = args.length === 0 ? this.connection.sendRequest(method) : this.connection.sendRequest(method, args[0]);
658
- const result = await Promise.race([requestPromise, timeoutPromise]);
659
- if (timeoutHandle !== null)
660
- clearTimeout(timeoutHandle);
1251
+ const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
661
1252
  return result;
662
1253
  } catch (error) {
663
- if (timeoutHandle !== null)
664
- clearTimeout(timeoutHandle);
665
1254
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
666
1255
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
667
1256
  `) || undefined);
@@ -670,6 +1259,9 @@ class LspClientTransport {
670
1259
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
671
1260
  }
672
1261
  throw error;
1262
+ } finally {
1263
+ clearTimeout(timeoutHandle);
1264
+ combinedSignal.dispose();
673
1265
  }
674
1266
  }
675
1267
  async sendNotification(method, ...args) {
@@ -698,17 +1290,17 @@ class LspClientTransport {
698
1290
  try {
699
1291
  await this.sendRequest("shutdown");
700
1292
  } catch (error) {
701
- reportBestEffortCleanupError("shutdown request", error);
1293
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
702
1294
  }
703
1295
  try {
704
1296
  await this.sendNotification("exit");
705
1297
  } catch (error) {
706
- reportBestEffortCleanupError("exit notification", error);
1298
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
707
1299
  }
708
1300
  try {
709
1301
  this.connection.dispose();
710
1302
  } catch (error) {
711
- reportBestEffortCleanupError("connection dispose", error);
1303
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
712
1304
  }
713
1305
  this.connection = null;
714
1306
  }
@@ -719,8 +1311,8 @@ class LspClientTransport {
719
1311
  try {
720
1312
  proc.kill();
721
1313
  let timeoutId;
722
- const timeoutPromise = new Promise((resolve6) => {
723
- timeoutId = setTimeout(resolve6, STOP_HARD_KILL_TIMEOUT_MS);
1314
+ const timeoutPromise = new Promise((resolve22) => {
1315
+ timeoutId = setTimeout(resolve22, STOP_HARD_KILL_TIMEOUT_MS);
724
1316
  });
725
1317
  await Promise.race([
726
1318
  proc.exited.then(() => {
@@ -736,14 +1328,14 @@ class LspClientTransport {
736
1328
  proc.kill("SIGKILL");
737
1329
  await Promise.race([
738
1330
  proc.exited,
739
- new Promise((resolve6) => setTimeout(resolve6, STOP_SIGKILL_GRACE_MS))
1331
+ new Promise((resolve22) => setTimeout(resolve22, STOP_SIGKILL_GRACE_MS))
740
1332
  ]);
741
1333
  } catch (error) {
742
- reportBestEffortCleanupError("hard process kill", error);
1334
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
743
1335
  }
744
1336
  }
745
1337
  } catch (error) {
746
- reportBestEffortCleanupError("process stop", error);
1338
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
747
1339
  }
748
1340
  }
749
1341
  this.processExited = true;
@@ -753,24 +1345,43 @@ class LspClientTransport {
753
1345
  return this.diagnosticsStore.get(uri) ?? [];
754
1346
  }
755
1347
  }
756
- function createLspSpawnEnv(_root, input) {
757
- return { ...input };
758
- }
759
- function isDiagnostic(value) {
760
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
761
- }
762
- function isRange(value) {
763
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
1348
+ function combineAbortSignals(primary, secondary) {
1349
+ const controller = new AbortController;
1350
+ const abortFrom = (signal) => {
1351
+ if (!controller.signal.aborted)
1352
+ controller.abort(signal.reason);
1353
+ };
1354
+ const onPrimaryAbort = () => {
1355
+ if (primary)
1356
+ abortFrom(primary);
1357
+ };
1358
+ const onSecondaryAbort = () => abortFrom(secondary);
1359
+ if (primary?.aborted)
1360
+ abortFrom(primary);
1361
+ else
1362
+ primary?.addEventListener("abort", onPrimaryAbort, { once: true });
1363
+ if (secondary.aborted)
1364
+ abortFrom(secondary);
1365
+ else
1366
+ secondary.addEventListener("abort", onSecondaryAbort, { once: true });
1367
+ return {
1368
+ signal: controller.signal,
1369
+ dispose: () => {
1370
+ primary?.removeEventListener("abort", onPrimaryAbort);
1371
+ secondary.removeEventListener("abort", onSecondaryAbort);
1372
+ }
1373
+ };
764
1374
  }
765
- function isPosition(value) {
766
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
1375
+ function supportsDiagnosticPull(capabilities) {
1376
+ if (capabilities === undefined)
1377
+ return false;
1378
+ return Object.hasOwn(capabilities, "diagnosticProvider");
767
1379
  }
768
- var INITIALIZE_SETTLE_MS = 300;
769
1380
 
770
1381
  class LspClientConnection extends LspClientTransport {
771
1382
  async initialize() {
772
1383
  const rootUri = pathToFileURL(this.root).href;
773
- await this.sendRequest("initialize", {
1384
+ const result = await this.sendRequest("initialize", {
774
1385
  processId: process.pid,
775
1386
  rootUri,
776
1387
  rootPath: this.root,
@@ -784,8 +1395,7 @@ class LspClientConnection extends LspClientTransport {
784
1395
  publishDiagnostics: {},
785
1396
  rename: {
786
1397
  prepareSupport: true,
787
- prepareSupportDefaultBehavior: 1,
788
- honorsChangeAnnotations: true
1398
+ prepareSupportDefaultBehavior: 1
789
1399
  },
790
1400
  codeAction: {
791
1401
  codeActionLiteralSupport: {
@@ -814,28 +1424,22 @@ class LspClientConnection extends LspClientTransport {
814
1424
  symbol: {},
815
1425
  workspaceFolders: true,
816
1426
  configuration: true,
817
- applyEdit: true,
1427
+ ...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
818
1428
  workspaceEdit: {
819
- documentChanges: true
1429
+ documentChanges: true,
1430
+ resourceOperations: ["create", "rename", "delete"]
820
1431
  }
821
1432
  }
822
1433
  },
823
1434
  initializationOptions: this.server.initialization
824
1435
  }, { timeoutMs: this.initializeTimeoutMs });
1436
+ this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
825
1437
  await this.sendNotification("initialized");
826
1438
  await this.sendNotification("workspace/didChangeConfiguration", {
827
1439
  settings: { json: { validate: { enable: true } } }
828
1440
  });
829
- await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
830
1441
  }
831
1442
  }
832
- var BASENAME_EXTENSIONS = {
833
- Dockerfile: ".dockerfile",
834
- Containerfile: ".dockerfile"
835
- };
836
- function effectiveExtension(filePath) {
837
- return BASENAME_EXTENSIONS[basename(filePath)] ?? extname(filePath);
838
- }
839
1443
  var SYMBOL_KIND_MAP = {
840
1444
  1: "File",
841
1445
  2: "Module",
@@ -1006,81 +1610,1384 @@ var EXT_TO_LANG = {
1006
1610
  function getLanguageId(ext) {
1007
1611
  return EXT_TO_LANG[ext] ?? "plaintext";
1008
1612
  }
1009
- var POST_OPEN_DELAY_MS = 1000;
1010
- var POST_DIAGNOSTICS_WAIT_MS = 500;
1613
+ var WATCHED_FILE_BATCH_SIZE = 128;
1614
+ var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1615
+ function canonicalPath(filePath) {
1616
+ const absolute = resolve2(filePath);
1617
+ try {
1618
+ return realpathSync2(absolute);
1619
+ } catch {
1620
+ return absolute;
1621
+ }
1622
+ }
1623
+ function isSameOrDescendant(candidate, parent) {
1624
+ const suffix = relative2(parent, candidate);
1625
+ return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
1626
+ }
1627
+ function movedPath(candidate, oldPath, newPath) {
1628
+ const suffix = relative2(oldPath, candidate);
1629
+ return suffix === "" ? newPath : resolve2(newPath, suffix);
1630
+ }
1011
1631
 
1012
- class LspClient extends LspClientConnection {
1013
- openedFiles = new Set;
1014
- documentVersions = new Map;
1015
- lastSyncedText = new Map;
1016
- diagnosticPullErrors = [];
1017
- getDiagnosticPullErrors() {
1018
- return this.diagnosticPullErrors;
1632
+ class WorkspaceDocumentState {
1633
+ sendNotification;
1634
+ clearDiagnostics;
1635
+ openDocuments = new Map;
1636
+ openByUri = new Map;
1637
+ openPromises = new Map;
1638
+ now;
1639
+ versionlessPublishQuiescenceMs;
1640
+ constructor(sendNotification, clearDiagnostics, options = {}) {
1641
+ this.sendNotification = sendNotification;
1642
+ this.clearDiagnostics = clearDiagnostics;
1643
+ this.now = options.now ?? (() => Date.now());
1644
+ this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1019
1645
  }
1020
1646
  async openFile(filePath) {
1021
- const absPath = resolve(contextCwd(), filePath);
1022
- const uri = pathToFileURL2(absPath).href;
1023
- const text = readFileSync(absPath, "utf-8");
1024
- if (!this.openedFiles.has(absPath)) {
1025
- const ext = effectiveExtension(absPath);
1026
- const languageId = getLanguageId(ext);
1027
- const version = 1;
1028
- await this.sendNotification("textDocument/didOpen", {
1029
- textDocument: {
1030
- uri,
1031
- languageId,
1032
- version,
1033
- text
1647
+ const path2 = canonicalPath(filePath);
1648
+ const existingOpen = this.openPromises.get(path2);
1649
+ if (existingOpen) {
1650
+ await existingOpen;
1651
+ return this.openFile(path2);
1652
+ }
1653
+ const text = readFileSync2(path2, "utf-8");
1654
+ const existing = this.openDocuments.get(path2);
1655
+ if (!existing)
1656
+ return this.openDocumentSingleFlight(path2, text);
1657
+ if (existing.text === text)
1658
+ return;
1659
+ await this.changeDocument(existing, text);
1660
+ }
1661
+ getVersion(filePath) {
1662
+ return this.openDocuments.get(canonicalPath(filePath))?.version;
1663
+ }
1664
+ getStoredDiagnostics(uri) {
1665
+ const state = this.openByUri.get(uri);
1666
+ if (!state)
1667
+ return [];
1668
+ return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
1669
+ }
1670
+ captureDiagnosticSnapshot(filePath) {
1671
+ const state = this.openDocuments.get(canonicalPath(filePath));
1672
+ if (!state)
1673
+ return null;
1674
+ return {
1675
+ path: state.path,
1676
+ uri: state.uri,
1677
+ version: state.version,
1678
+ documentGeneration: state.generation,
1679
+ publishGeneration: state.publishGeneration
1680
+ };
1681
+ }
1682
+ isCurrentSnapshot(snapshot) {
1683
+ const state = this.openDocuments.get(snapshot.path);
1684
+ return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1685
+ }
1686
+ getPullCache(snapshot) {
1687
+ const state = this.openByUri.get(snapshot.uri);
1688
+ if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1689
+ return null;
1690
+ return state.pullCache;
1691
+ }
1692
+ recordPullDiagnostics(snapshot, report) {
1693
+ const state = this.openByUri.get(snapshot.uri);
1694
+ if (!state)
1695
+ return;
1696
+ state.pullCache = {
1697
+ documentVersion: snapshot.version,
1698
+ diagnostics: [...report.diagnostics],
1699
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
1700
+ };
1701
+ }
1702
+ recordPublishedDiagnostics(params) {
1703
+ const state = this.openByUri.get(params.uri);
1704
+ if (!state)
1705
+ return;
1706
+ state.publishGeneration += 1;
1707
+ state.lastPublish = {
1708
+ diagnostics: [...params.diagnostics],
1709
+ publishGeneration: state.publishGeneration,
1710
+ documentGenerationAtArrival: state.generation,
1711
+ arrivedAt: this.now(),
1712
+ ...params.version === undefined ? {} : { version: params.version }
1713
+ };
1714
+ this.notifyWaiters(state);
1715
+ }
1716
+ resolvePushDiagnostics(snapshot) {
1717
+ const state = this.openByUri.get(snapshot.uri);
1718
+ if (!state?.lastPublish)
1719
+ return { status: "missing" };
1720
+ const publish = state.lastPublish;
1721
+ if (publish.version !== undefined) {
1722
+ return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
1723
+ }
1724
+ if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
1725
+ return { status: "missing" };
1726
+ const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
1727
+ const waitMs = Math.max(0, readyAt - this.now());
1728
+ return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1729
+ }
1730
+ waitForDiagnosticsActivity(snapshot, timeoutMs) {
1731
+ const state = this.openByUri.get(snapshot.uri);
1732
+ if (!state || timeoutMs <= 0)
1733
+ return Promise.resolve();
1734
+ return new Promise((resolveActivity) => {
1735
+ let settled = false;
1736
+ const finish = () => {
1737
+ if (settled)
1738
+ return;
1739
+ settled = true;
1740
+ clearTimeout(timer);
1741
+ state.waiters.delete(finish);
1742
+ resolveActivity();
1743
+ };
1744
+ const timer = setTimeout(finish, timeoutMs);
1745
+ if (typeof timer.unref === "function")
1746
+ timer.unref();
1747
+ state.waiters.add(finish);
1748
+ });
1749
+ }
1750
+ validateVersions(operations) {
1751
+ const versions = new Map([...this.openDocuments].map(([path2, state]) => [path2, state.version]));
1752
+ for (const operation of operations) {
1753
+ if (operation.kind === "text") {
1754
+ const current = versions.get(operation.path);
1755
+ if (operation.documentVersion !== null && current !== operation.documentVersion) {
1756
+ const observed = current === undefined ? "closed document" : `open document version ${current}`;
1757
+ return {
1758
+ changeIndex: operation.changeIndex,
1759
+ message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
1760
+ };
1761
+ }
1762
+ if (current !== undefined)
1763
+ versions.set(operation.path, current + 1);
1764
+ continue;
1765
+ }
1766
+ if (operation.kind === "rename") {
1767
+ const moved = [...versions].filter(([path2]) => isSameOrDescendant(path2, operation.oldPath));
1768
+ for (const [path2] of moved)
1769
+ versions.delete(path2);
1770
+ for (const [path2] of moved)
1771
+ versions.set(movedPath(path2, operation.oldPath, operation.newPath), 1);
1772
+ continue;
1773
+ }
1774
+ if (operation.kind === "delete") {
1775
+ for (const path2 of [...versions.keys()]) {
1776
+ if (isSameOrDescendant(path2, operation.path))
1777
+ versions.delete(path2);
1034
1778
  }
1779
+ continue;
1780
+ }
1781
+ if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
1782
+ versions.set(operation.path, 1);
1783
+ }
1784
+ }
1785
+ return null;
1786
+ }
1787
+ async synchronize(delta) {
1788
+ const watched = [];
1789
+ for (const mutation of delta.operations)
1790
+ await this.synchronizeMutation(mutation, watched);
1791
+ for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
1792
+ await this.sendNotification("workspace/didChangeWatchedFiles", {
1793
+ changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
1035
1794
  });
1036
- this.openedFiles.add(absPath);
1037
- this.documentVersions.set(uri, version);
1038
- this.lastSyncedText.set(uri, text);
1039
- await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
1795
+ }
1796
+ }
1797
+ async synchronizeMutation(mutation, watched) {
1798
+ if (mutation.kind === "text") {
1799
+ const state = this.openDocuments.get(mutation.path);
1800
+ if (state)
1801
+ await this.changeDocument(state, mutation.afterText);
1802
+ else
1803
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
1040
1804
  return;
1041
1805
  }
1042
- const prevText = this.lastSyncedText.get(uri);
1043
- if (prevText === text) {
1806
+ if (mutation.kind === "create") {
1807
+ const state = this.openDocuments.get(mutation.path);
1808
+ if (state) {
1809
+ await this.closeDocument(state);
1810
+ await this.openDocumentSingleFlight(mutation.path, readFileSync2(mutation.path, "utf-8"));
1811
+ } else {
1812
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
1813
+ }
1044
1814
  return;
1045
1815
  }
1046
- const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
1047
- this.documentVersions.set(uri, nextVersion);
1048
- this.lastSyncedText.set(uri, text);
1816
+ if (mutation.kind === "rename") {
1817
+ const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
1818
+ for (const state of moved)
1819
+ await this.closeDocument(state);
1820
+ for (const state of moved) {
1821
+ const path2 = movedPath(state.path, mutation.oldPath, mutation.newPath);
1822
+ await this.openDocumentSingleFlight(path2, readFileSync2(path2, "utf-8"));
1823
+ }
1824
+ if (moved.length === 0) {
1825
+ watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
1826
+ watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
1827
+ }
1828
+ return;
1829
+ }
1830
+ const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
1831
+ for (const state of removed)
1832
+ await this.closeDocument(state);
1833
+ if (removed.length === 0)
1834
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
1835
+ }
1836
+ async openDocumentSingleFlight(path2, text) {
1837
+ const existing = this.openPromises.get(path2);
1838
+ if (existing)
1839
+ return existing;
1840
+ const open = (async () => {
1841
+ const state = {
1842
+ path: path2,
1843
+ uri: pathToFileURL2(path2).href,
1844
+ languageId: getLanguageId(effectiveExtension(path2)),
1845
+ text,
1846
+ version: 1,
1847
+ generation: 1,
1848
+ publishGeneration: 0,
1849
+ waiters: new Set
1850
+ };
1851
+ this.openDocuments.set(path2, state);
1852
+ this.openByUri.set(state.uri, state);
1853
+ this.notifyWaiters(state);
1854
+ await this.sendNotification("textDocument/didOpen", {
1855
+ textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
1856
+ });
1857
+ })().finally(() => {
1858
+ this.openPromises.delete(path2);
1859
+ });
1860
+ this.openPromises.set(path2, open);
1861
+ return open;
1862
+ }
1863
+ async changeDocument(state, text) {
1864
+ state.text = text;
1865
+ state.version += 1;
1866
+ state.generation += 1;
1867
+ this.clearDiagnostics(state.uri);
1868
+ this.notifyWaiters(state);
1049
1869
  await this.sendNotification("textDocument/didChange", {
1050
- textDocument: { uri, version: nextVersion },
1870
+ textDocument: { uri: state.uri, version: state.version },
1051
1871
  contentChanges: [{ text }]
1052
1872
  });
1053
- await this.sendNotification("textDocument/didSave", {
1054
- textDocument: { uri },
1055
- text
1873
+ await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
1874
+ }
1875
+ async closeDocument(state) {
1876
+ this.openDocuments.delete(state.path);
1877
+ this.openByUri.delete(state.uri);
1878
+ this.clearDiagnostics(state.uri);
1879
+ this.notifyWaiters(state);
1880
+ await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
1881
+ }
1882
+ notifyWaiters(state) {
1883
+ for (const waiter of [...state.waiters])
1884
+ waiter();
1885
+ }
1886
+ }
1887
+ var CONCURRENT_FAILURE_REASON_BY_PHASE = {
1888
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
1889
+ settled: "workspace/applyEdit was already handled for this workspace mutation"
1890
+ };
1891
+ function workspaceApplyEditConcurrentFailureReason(phase) {
1892
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
1893
+ }
1894
+
1895
+ class WorkspaceEditPathError extends Error {
1896
+ path;
1897
+ detail;
1898
+ name = "WorkspaceEditPathError";
1899
+ constructor(path2, detail) {
1900
+ super(`${detail}: ${path2}`);
1901
+ this.path = path2;
1902
+ this.detail = detail;
1903
+ }
1904
+ }
1905
+ function isPathInsideWorkspace(filePath, workspaceRoot) {
1906
+ const relativePath = relative3(workspaceRoot, filePath);
1907
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
1908
+ }
1909
+ function canonicalizeMissingPath(filePath) {
1910
+ let ancestor = filePath;
1911
+ while (!existsSync3(ancestor)) {
1912
+ const parent = dirname4(ancestor);
1913
+ if (parent === ancestor)
1914
+ throw new WorkspaceEditPathError(filePath, "no existing ancestor");
1915
+ ancestor = parent;
1916
+ }
1917
+ return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
1918
+ }
1919
+ function canonicalWorkspaceRoot(workspaceRoot) {
1920
+ try {
1921
+ const canonical = realpathSync3(resolve3(workspaceRoot));
1922
+ if (!lstatSync2(canonical).isDirectory()) {
1923
+ return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
1924
+ }
1925
+ return {
1926
+ success: true,
1927
+ path: canonical,
1928
+ requestedPath: resolve3(workspaceRoot),
1929
+ followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync2(resolve3(workspaceRoot)).isSymbolicLink()
1930
+ };
1931
+ } catch (error) {
1932
+ const detail = error instanceof Error ? error.message : String(error);
1933
+ return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
1934
+ }
1935
+ }
1936
+ function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
1937
+ let requestedPath;
1938
+ try {
1939
+ const parsed = new URL(uri);
1940
+ if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
1941
+ return { success: false, error: `non-file URI ${uri}` };
1942
+ }
1943
+ requestedPath = resolve3(fileURLToPath2(parsed));
1944
+ } catch (error) {
1945
+ const detail = error instanceof Error ? error.message : String(error);
1946
+ return { success: false, error: `non-file URI ${uri}: ${detail}` };
1947
+ }
1948
+ try {
1949
+ const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
1950
+ if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
1951
+ return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
1952
+ }
1953
+ return {
1954
+ success: true,
1955
+ path: canonical,
1956
+ requestedPath,
1957
+ followedSymbolicLink: existsSync3(requestedPath) && lstatSync2(requestedPath).isSymbolicLink()
1958
+ };
1959
+ } catch (error) {
1960
+ const detail = error instanceof Error ? error.message : String(error);
1961
+ return { success: false, error: `${requestedPath}: ${detail}` };
1962
+ }
1963
+ }
1964
+ function snapshotPath(path2, includeChildren) {
1965
+ if (!existsSync3(path2))
1966
+ return { kind: "missing" };
1967
+ const stats = lstatSync2(path2);
1968
+ if (stats.isFile())
1969
+ return { kind: "file", content: readFileSync3(path2, "utf-8") };
1970
+ if (stats.isDirectory()) {
1971
+ return includeChildren ? { kind: "directory", children: readdirSync(path2).sort() } : { kind: "directory" };
1972
+ }
1973
+ throw new WorkspaceEditPathError(path2, "unsupported filesystem entry");
1974
+ }
1975
+ var DEFAULT_IO = {
1976
+ writeFile(path2, content) {
1977
+ writeFileSync(path2, content, "utf-8");
1978
+ },
1979
+ rename(oldPath, newPath) {
1980
+ renameSync(oldPath, newPath);
1981
+ },
1982
+ remove(path2, recursive) {
1983
+ rmSync(path2, { recursive, force: false });
1984
+ }
1985
+ };
1986
+ function snapshotsEqual(expected, actual) {
1987
+ if (expected.kind !== actual.kind)
1988
+ return false;
1989
+ if (expected.kind === "file" && actual.kind === "file")
1990
+ return expected.content === actual.content;
1991
+ if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
1992
+ return JSON.stringify(expected.children) === JSON.stringify(actual.children);
1993
+ }
1994
+ return true;
1995
+ }
1996
+ function liveSnapshot(path2, expected) {
1997
+ return snapshotPath(path2, expected.kind === "directory" && expected.children !== undefined);
1998
+ }
1999
+ function firstOperationIndex(plan) {
2000
+ return plan.operations[0]?.changeIndex ?? 0;
2001
+ }
2002
+ function failedCommit(plan, failure) {
2003
+ const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
2004
+ return {
2005
+ result: {
2006
+ success: false,
2007
+ filesModified,
2008
+ totalEdits,
2009
+ errors: [`change ${changeIndex}: ${message}`],
2010
+ failedChange: changeIndex,
2011
+ ...lateAbort ? { lateAbort: true } : {}
2012
+ },
2013
+ delta: mutationDelta(mutations),
2014
+ fingerprint: plan.fingerprint
2015
+ };
2016
+ }
2017
+ function verifySnapshots(plan) {
2018
+ for (const [path2, expected] of plan.snapshots) {
2019
+ let actual;
2020
+ try {
2021
+ actual = liveSnapshot(path2, expected);
2022
+ } catch (error) {
2023
+ const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
2024
+ const detail = error instanceof Error ? error.message : String(error);
2025
+ return failedCommit(plan, { message: `cannot verify snapshot for ${path2}: ${detail}`, changeIndex });
2026
+ }
2027
+ if (!snapshotsEqual(expected, actual)) {
2028
+ const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
2029
+ return failedCommit(plan, { message: `workspace state changed before commit: ${path2}`, changeIndex });
2030
+ }
2031
+ }
2032
+ return null;
2033
+ }
2034
+ function addModifiedPath(paths, path2) {
2035
+ if (!paths.includes(path2))
2036
+ paths.push(path2);
2037
+ }
2038
+ function reportedPath(plan, path2) {
2039
+ return plan.reportedPathByCanonical.get(path2) ?? path2;
2040
+ }
2041
+ function changedPathsForMutation(mutation) {
2042
+ return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
2043
+ }
2044
+ function mutationDelta(operations) {
2045
+ const changedPaths = new Set;
2046
+ for (const operation of operations) {
2047
+ for (const path2 of changedPathsForMutation(operation))
2048
+ changedPaths.add(path2);
2049
+ }
2050
+ return { operations, changedPaths: [...changedPaths].sort() };
2051
+ }
2052
+ function resolveIo(overrides) {
2053
+ return {
2054
+ writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
2055
+ rename: overrides?.rename ?? DEFAULT_IO.rename,
2056
+ remove: overrides?.remove ?? DEFAULT_IO.remove
2057
+ };
2058
+ }
2059
+ function commitOperation(context, operation) {
2060
+ const { plan, io, accumulator } = context;
2061
+ if (operation.kind === "noop")
2062
+ return;
2063
+ if (operation.kind === "text") {
2064
+ io.writeFile(operation.path, operation.afterText);
2065
+ accumulator.mutations.push({
2066
+ kind: "text",
2067
+ path: operation.path,
2068
+ beforeText: operation.beforeText,
2069
+ afterText: operation.afterText
2070
+ });
2071
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
2072
+ accumulator.totalEdits += operation.editCount;
2073
+ return;
2074
+ }
2075
+ if (operation.kind === "create") {
2076
+ io.writeFile(operation.path, "");
2077
+ accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
2078
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
2079
+ return;
2080
+ }
2081
+ if (operation.kind === "rename") {
2082
+ if (operation.replaceDestination) {
2083
+ const targetKind = existsSync4(operation.newPath) && lstatSync3(operation.newPath).isDirectory() ? "directory" : "file";
2084
+ io.remove(operation.newPath, targetKind === "directory");
2085
+ accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
2086
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
2087
+ }
2088
+ io.rename(operation.oldPath, operation.newPath);
2089
+ accumulator.mutations.push({
2090
+ kind: "rename",
2091
+ oldPath: operation.oldPath,
2092
+ newPath: operation.newPath,
2093
+ sourceKind: operation.sourceKind
2094
+ });
2095
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
2096
+ return;
2097
+ }
2098
+ io.remove(operation.path, operation.recursive);
2099
+ accumulator.mutations.push({
2100
+ kind: "delete",
2101
+ path: operation.path,
2102
+ targetKind: operation.targetKind
2103
+ });
2104
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
2105
+ }
2106
+ function commitWorkspaceEditPlan(plan, options = {}) {
2107
+ if (options.signal?.aborted) {
2108
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2109
+ }
2110
+ const stale = verifySnapshots(plan);
2111
+ if (stale)
2112
+ return stale;
2113
+ if (options.signal?.aborted) {
2114
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2115
+ }
2116
+ const io = resolveIo(options.io);
2117
+ const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
2118
+ const context = { plan, io, accumulator };
2119
+ let lateAbort = false;
2120
+ for (const operation of plan.operations) {
2121
+ try {
2122
+ commitOperation(context, operation);
2123
+ } catch (error) {
2124
+ const detail = error instanceof Error ? error.message : String(error);
2125
+ return failedCommit(plan, {
2126
+ message: `I/O failure during ${operation.kind}: ${detail}`,
2127
+ changeIndex: operation.changeIndex,
2128
+ mutations: accumulator.mutations,
2129
+ filesModified: accumulator.filesModified,
2130
+ totalEdits: accumulator.totalEdits,
2131
+ lateAbort: lateAbort || options.signal?.aborted === true
2132
+ });
2133
+ }
2134
+ if (options.signal?.aborted)
2135
+ lateAbort = true;
2136
+ }
2137
+ const result = {
2138
+ success: true,
2139
+ filesModified: accumulator.filesModified,
2140
+ totalEdits: accumulator.totalEdits,
2141
+ errors: [],
2142
+ ...lateAbort ? { lateAbort: true } : {}
2143
+ };
2144
+ return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
2145
+ }
2146
+ function canonicalFingerprint(operations) {
2147
+ const canonical = operations.map((operation) => {
2148
+ switch (operation.kind) {
2149
+ case "text":
2150
+ return {
2151
+ kind: operation.kind,
2152
+ changeIndex: operation.changeIndex,
2153
+ path: operation.path,
2154
+ edits: operation.edits,
2155
+ version: operation.version
2156
+ };
2157
+ case "rename":
2158
+ return {
2159
+ kind: operation.kind,
2160
+ changeIndex: operation.changeIndex,
2161
+ oldPath: operation.oldPath,
2162
+ newPath: operation.newPath,
2163
+ overwrite: operation.overwrite,
2164
+ ignoreIfExists: operation.ignoreIfExists
2165
+ };
2166
+ case "create":
2167
+ return {
2168
+ kind: operation.kind,
2169
+ changeIndex: operation.changeIndex,
2170
+ path: operation.path,
2171
+ overwrite: operation.overwrite,
2172
+ ignoreIfExists: operation.ignoreIfExists
2173
+ };
2174
+ case "delete":
2175
+ return {
2176
+ kind: operation.kind,
2177
+ changeIndex: operation.changeIndex,
2178
+ path: operation.path,
2179
+ recursive: operation.recursive,
2180
+ ignoreIfNotExists: operation.ignoreIfNotExists
2181
+ };
2182
+ }
2183
+ });
2184
+ return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
2185
+ }
2186
+
2187
+ class WorkspaceEditValidationError extends Error {
2188
+ changeIndex;
2189
+ detail;
2190
+ name = "WorkspaceEditValidationError";
2191
+ constructor(changeIndex, detail) {
2192
+ super(`change ${changeIndex}: ${detail}`);
2193
+ this.changeIndex = changeIndex;
2194
+ this.detail = detail;
2195
+ }
2196
+ }
2197
+ function isRecord3(value) {
2198
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2199
+ }
2200
+ function parsePosition(value) {
2201
+ if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
2202
+ return null;
2203
+ }
2204
+ return { line: value["line"], character: value["character"] };
2205
+ }
2206
+ function parseRange(value) {
2207
+ if (!isRecord3(value))
2208
+ return null;
2209
+ const start = parsePosition(value["start"]);
2210
+ const end = parsePosition(value["end"]);
2211
+ return start && end ? { start, end } : null;
2212
+ }
2213
+ function parseTextEdits(value, changeIndex) {
2214
+ if (!Array.isArray(value)) {
2215
+ throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
2216
+ }
2217
+ const edits = [];
2218
+ for (const candidate of value) {
2219
+ if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
2220
+ throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
2221
+ }
2222
+ if ("annotationId" in candidate) {
2223
+ throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
2224
+ }
2225
+ const range = parseRange(candidate["range"]);
2226
+ if (!range)
2227
+ throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
2228
+ edits.push({ range, newText: candidate["newText"] });
2229
+ }
2230
+ return edits;
2231
+ }
2232
+ function parseBooleanOption(options, key, changeIndex) {
2233
+ const value = options[key];
2234
+ if (value === undefined)
2235
+ return false;
2236
+ if (typeof value !== "boolean") {
2237
+ throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
2238
+ }
2239
+ return value;
2240
+ }
2241
+ function parseOptions(value, allowed, changeIndex) {
2242
+ if (value === undefined)
2243
+ return {};
2244
+ if (!isRecord3(value))
2245
+ throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
2246
+ for (const key of Object.keys(value)) {
2247
+ if (!allowed.includes(key))
2248
+ throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
2249
+ }
2250
+ const parsed = {};
2251
+ for (const key of allowed)
2252
+ parsed[key] = parseBooleanOption(value, key, changeIndex);
2253
+ return parsed;
2254
+ }
2255
+ function parseResourceChange(input) {
2256
+ const kind = input.change["kind"];
2257
+ if (kind === "create" || kind === "delete") {
2258
+ parseSinglePathResource(input, kind);
2259
+ return;
2260
+ }
2261
+ if (kind !== "rename") {
2262
+ throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
2263
+ }
2264
+ parseRename(input);
2265
+ }
2266
+ function parseSinglePathResource(input, kind) {
2267
+ const { change, changeIndex, workspaceRoot, target } = input;
2268
+ if (typeof change["uri"] !== "string")
2269
+ throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
2270
+ const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
2271
+ if (!resolvedPath.success) {
2272
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2273
+ return;
2274
+ }
2275
+ if (kind === "create") {
2276
+ const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2277
+ target.operations.push({
2278
+ kind,
2279
+ changeIndex,
2280
+ path: resolvedPath.path,
2281
+ reportedPath: resolvedPath.requestedPath,
2282
+ overwrite: options2["overwrite"] ?? false,
2283
+ ignoreIfExists: options2["ignoreIfExists"] ?? false,
2284
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2285
+ });
2286
+ return;
2287
+ }
2288
+ const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
2289
+ target.operations.push({
2290
+ kind,
2291
+ changeIndex,
2292
+ path: resolvedPath.path,
2293
+ reportedPath: resolvedPath.requestedPath,
2294
+ recursive: options["recursive"] ?? false,
2295
+ ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
2296
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2297
+ });
2298
+ }
2299
+ function parseRename(input) {
2300
+ const { change, changeIndex, workspaceRoot, target } = input;
2301
+ if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
2302
+ throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
2303
+ }
2304
+ const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
2305
+ const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
2306
+ if (!oldPath.success || !newPath.success) {
2307
+ target.failures.push({
2308
+ changeIndex,
2309
+ message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
2310
+ });
2311
+ return;
2312
+ }
2313
+ const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2314
+ target.operations.push({
2315
+ kind: "rename",
2316
+ changeIndex,
2317
+ oldPath: oldPath.path,
2318
+ newPath: newPath.path,
2319
+ reportedOldPath: oldPath.requestedPath,
2320
+ reportedNewPath: newPath.requestedPath,
2321
+ overwrite: options["overwrite"] ?? false,
2322
+ ignoreIfExists: options["ignoreIfExists"] ?? false,
2323
+ followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
2324
+ });
2325
+ }
2326
+ function failureResult(failures) {
2327
+ const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
2328
+ const first = sorted[0];
2329
+ return {
2330
+ success: false,
2331
+ filesModified: [],
2332
+ totalEdits: 0,
2333
+ errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
2334
+ ...first ? { failedChange: first.changeIndex } : {}
2335
+ };
2336
+ }
2337
+ function parseWorkspaceEdit(edit, workspaceRoot) {
2338
+ if (!isRecord3(edit))
2339
+ return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
2340
+ if (edit["changeAnnotations"] !== undefined) {
2341
+ return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
2342
+ }
2343
+ const hasChanges = edit["changes"] !== undefined;
2344
+ const hasDocumentChanges = edit["documentChanges"] !== undefined;
2345
+ if (hasChanges && hasDocumentChanges) {
2346
+ return {
2347
+ operations: [],
2348
+ failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
2349
+ };
2350
+ }
2351
+ const target = { operations: [], failures: [] };
2352
+ if (hasChanges)
2353
+ return parseChanges(edit["changes"], workspaceRoot, target);
2354
+ return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
2355
+ }
2356
+ function parseChanges(value, workspaceRoot, target) {
2357
+ if (!isRecord3(value))
2358
+ return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
2359
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
2360
+ for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
2361
+ const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
2362
+ if (!resolvedPath.success) {
2363
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2364
+ continue;
2365
+ }
2366
+ try {
2367
+ target.operations.push({
2368
+ kind: "text",
2369
+ changeIndex,
2370
+ path: resolvedPath.path,
2371
+ reportedPath: resolvedPath.requestedPath,
2372
+ edits: parseTextEdits(rawEdits, changeIndex),
2373
+ version: null
2374
+ });
2375
+ } catch (error) {
2376
+ if (error instanceof WorkspaceEditValidationError) {
2377
+ target.failures.push({ changeIndex, message: error.detail });
2378
+ continue;
2379
+ }
2380
+ throw error;
2381
+ }
2382
+ }
2383
+ return target;
2384
+ }
2385
+ function parseDocumentChanges(value, workspaceRoot, target) {
2386
+ if (value === undefined)
2387
+ return target;
2388
+ if (!Array.isArray(value)) {
2389
+ return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
2390
+ }
2391
+ for (const [changeIndex, change] of value.entries()) {
2392
+ try {
2393
+ parseDocumentChange({ change, changeIndex, workspaceRoot, target });
2394
+ } catch (error) {
2395
+ if (error instanceof WorkspaceEditValidationError) {
2396
+ target.failures.push({ changeIndex, message: error.detail });
2397
+ continue;
2398
+ }
2399
+ throw error;
2400
+ }
2401
+ }
2402
+ return target;
2403
+ }
2404
+ function parseDocumentChange(input) {
2405
+ const { change, changeIndex, workspaceRoot, target } = input;
2406
+ if (!isRecord3(change))
2407
+ throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
2408
+ if ("annotationId" in change) {
2409
+ throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
2410
+ }
2411
+ if (typeof change["kind"] === "string") {
2412
+ parseResourceChange({ change, changeIndex, workspaceRoot, target });
2413
+ return;
2414
+ }
2415
+ const identifier = change["textDocument"];
2416
+ if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
2417
+ throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
2418
+ }
2419
+ const version = identifier["version"];
2420
+ if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
2421
+ throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
2422
+ }
2423
+ const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
2424
+ if (!resolvedPath.success) {
2425
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2426
+ return;
2427
+ }
2428
+ target.operations.push({
2429
+ kind: "text",
2430
+ changeIndex,
2431
+ path: resolvedPath.path,
2432
+ reportedPath: resolvedPath.requestedPath,
2433
+ edits: parseTextEdits(change["edits"], changeIndex),
2434
+ version
2435
+ });
2436
+ }
2437
+ function comparePosition(left, right) {
2438
+ return left.line === right.line ? left.character - right.character : left.line - right.line;
2439
+ }
2440
+ function positionsEqual(left, right) {
2441
+ return left.line === right.line && left.character === right.character;
2442
+ }
2443
+ function rangesEqual(left, right) {
2444
+ return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
2445
+ }
2446
+ function isEmptyRange(range) {
2447
+ return positionsEqual(range.start, range.end);
2448
+ }
2449
+ function formatRange(range) {
2450
+ return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
2451
+ }
2452
+ function validatePosition(position, label, context) {
2453
+ const { lines, changeIndex } = context;
2454
+ if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
2455
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
2456
+ }
2457
+ if (position.line < 0 || position.character < 0) {
2458
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
2459
+ }
2460
+ const line = lines[position.line];
2461
+ if (line === undefined) {
2462
+ throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
2463
+ }
2464
+ if (position.character > line.length) {
2465
+ throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
2466
+ }
2467
+ }
2468
+ function validateRange(range, lines, changeIndex) {
2469
+ const context = { lines, changeIndex };
2470
+ validatePosition(range.start, "start", context);
2471
+ validatePosition(range.end, "end", context);
2472
+ if (comparePosition(range.start, range.end) > 0) {
2473
+ throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
2474
+ }
2475
+ }
2476
+ function sortAndDeduplicate(edits) {
2477
+ const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
2478
+ const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
2479
+ return positionOrder === 0 ? right.index - left.index : positionOrder;
2480
+ });
2481
+ const unique = [];
2482
+ for (const entry of sorted) {
2483
+ const previous = unique.at(-1);
2484
+ if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
2485
+ continue;
2486
+ }
2487
+ unique.push(entry.edit);
2488
+ }
2489
+ return unique;
2490
+ }
2491
+ function validateNoOverlap(edits, changeIndex) {
2492
+ for (let index = 0;index < edits.length - 1; index += 1) {
2493
+ const later = edits[index];
2494
+ const earlier = edits[index + 1];
2495
+ if (later === undefined || earlier === undefined)
2496
+ continue;
2497
+ if (comparePosition(earlier.range.end, later.range.start) > 0) {
2498
+ throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
2499
+ }
2500
+ }
2501
+ }
2502
+ function applyNormalizedTextEdits(content, edits) {
2503
+ const lines = content.split(`
2504
+ `);
2505
+ for (const edit of edits) {
2506
+ const { start, end } = edit.range;
2507
+ const startLine = lines[start.line];
2508
+ const endLine = lines[end.line];
2509
+ if (startLine === undefined || endLine === undefined)
2510
+ continue;
2511
+ const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
2512
+ lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
2513
+ `));
2514
+ }
2515
+ return lines.join(`
2516
+ `);
2517
+ }
2518
+ function normalizeTextEdits(content, edits, changeIndex) {
2519
+ const lines = content.split(`
2520
+ `);
2521
+ for (const edit of edits) {
2522
+ validateRange(edit.range, lines, changeIndex);
2523
+ }
2524
+ const normalized = sortAndDeduplicate(edits);
2525
+ validateNoOverlap(normalized, changeIndex);
2526
+ return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
2527
+ }
2528
+ function isSameOrDescendant2(candidate, parent) {
2529
+ const relativePath = relative4(parent, candidate);
2530
+ return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
2531
+ }
2532
+ function removeVirtualSubtree(virtual, path2) {
2533
+ for (const candidate of [...virtual.keys()]) {
2534
+ if (isSameOrDescendant2(candidate, path2))
2535
+ virtual.delete(candidate);
2536
+ }
2537
+ virtual.set(path2, { kind: "missing" });
2538
+ }
2539
+ function moveVirtualSubtree(virtual, oldPath, newPath) {
2540
+ const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
2541
+ removeVirtualSubtree(virtual, oldPath);
2542
+ removeVirtualSubtree(virtual, newPath);
2543
+ for (const [candidate, entry] of moved) {
2544
+ const suffix = relative4(oldPath, candidate);
2545
+ virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
2546
+ }
2547
+ }
2548
+ function virtualDirectoryHasChildren(virtual, path2) {
2549
+ for (const [candidate, entry] of virtual) {
2550
+ if (candidate !== path2 && entry.kind !== "missing" && isSameOrDescendant2(candidate, path2))
2551
+ return true;
2552
+ }
2553
+ return false;
2554
+ }
2555
+ function requireVirtualParent(virtual, path2, changeIndex) {
2556
+ if (virtual.get(dirname5(path2))?.kind !== "directory") {
2557
+ throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path2}`);
2558
+ }
2559
+ }
2560
+ function simulateOperations(parsed, snapshots) {
2561
+ const virtual = new Map(snapshots);
2562
+ const planned = [];
2563
+ const failures = [];
2564
+ for (const operation of parsed) {
2565
+ try {
2566
+ planned.push(simulateOperation(operation, virtual));
2567
+ } catch (error) {
2568
+ if (error instanceof WorkspaceEditValidationError) {
2569
+ failures.push({ changeIndex: operation.changeIndex, message: error.detail });
2570
+ continue;
2571
+ }
2572
+ throw error;
2573
+ }
2574
+ }
2575
+ return { operations: planned, failures };
2576
+ }
2577
+ function simulateOperation(operation, virtual) {
2578
+ switch (operation.kind) {
2579
+ case "text":
2580
+ return simulateText(operation, virtual);
2581
+ case "create":
2582
+ return simulateCreate(operation, virtual);
2583
+ case "rename":
2584
+ return simulateRename(operation, virtual);
2585
+ case "delete":
2586
+ return simulateDelete(operation, virtual);
2587
+ }
2588
+ }
2589
+ function rejectSymbolicLink(operation) {
2590
+ if (operation.followedSymbolicLink) {
2591
+ throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
2592
+ }
2593
+ }
2594
+ function simulateText(operation, virtual) {
2595
+ const entry = virtual.get(operation.path);
2596
+ if (entry?.kind !== "file")
2597
+ throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
2598
+ const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
2599
+ virtual.set(operation.path, { kind: "file", content: normalized.text });
2600
+ return {
2601
+ kind: "text",
2602
+ changeIndex: operation.changeIndex,
2603
+ path: operation.path,
2604
+ beforeText: entry.content,
2605
+ afterText: normalized.text,
2606
+ editCount: normalized.edits.length,
2607
+ documentVersion: operation.version
2608
+ };
2609
+ }
2610
+ function simulateCreate(operation, virtual) {
2611
+ rejectSymbolicLink(operation);
2612
+ requireVirtualParent(virtual, operation.path, operation.changeIndex);
2613
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2614
+ if (target.kind !== "missing") {
2615
+ if (operation.overwrite && target.kind === "file") {
2616
+ virtual.set(operation.path, { kind: "file", content: "" });
2617
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
2618
+ }
2619
+ if (operation.ignoreIfExists)
2620
+ return { kind: "noop", changeIndex: operation.changeIndex };
2621
+ throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
2622
+ }
2623
+ virtual.set(operation.path, { kind: "file", content: "" });
2624
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
2625
+ }
2626
+ function simulateRename(operation, virtual) {
2627
+ rejectSymbolicLink(operation);
2628
+ const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
2629
+ if (source.kind === "missing") {
2630
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
2631
+ }
2632
+ if (operation.oldPath === operation.newPath)
2633
+ return { kind: "noop", changeIndex: operation.changeIndex };
2634
+ if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
2635
+ throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
2636
+ }
2637
+ requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
2638
+ const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
2639
+ if (destination.kind !== "missing" && !operation.overwrite) {
2640
+ if (operation.ignoreIfExists)
2641
+ return { kind: "noop", changeIndex: operation.changeIndex };
2642
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
2643
+ }
2644
+ moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
2645
+ return {
2646
+ kind: "rename",
2647
+ changeIndex: operation.changeIndex,
2648
+ oldPath: operation.oldPath,
2649
+ newPath: operation.newPath,
2650
+ sourceKind: source.kind,
2651
+ replaceDestination: destination.kind !== "missing"
2652
+ };
2653
+ }
2654
+ function simulateDelete(operation, virtual) {
2655
+ rejectSymbolicLink(operation);
2656
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2657
+ if (target.kind === "missing") {
2658
+ if (operation.ignoreIfNotExists)
2659
+ return { kind: "noop", changeIndex: operation.changeIndex };
2660
+ throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
2661
+ }
2662
+ if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
2663
+ throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
2664
+ }
2665
+ removeVirtualSubtree(virtual, operation.path);
2666
+ return {
2667
+ kind: "delete",
2668
+ changeIndex: operation.changeIndex,
2669
+ path: operation.path,
2670
+ targetKind: target.kind,
2671
+ recursive: operation.recursive
2672
+ };
2673
+ }
2674
+
2675
+ class WorkspaceSnapshotBuilder {
2676
+ workspaceRoot;
2677
+ snapshots = new Map;
2678
+ constructor(workspaceRoot) {
2679
+ this.workspaceRoot = workspaceRoot;
2680
+ }
2681
+ build(operations) {
2682
+ this.add(this.workspaceRoot, false);
2683
+ for (const operation of operations) {
2684
+ switch (operation.kind) {
2685
+ case "rename":
2686
+ this.add(operation.oldPath, true);
2687
+ this.add(operation.newPath, true);
2688
+ break;
2689
+ case "delete":
2690
+ this.add(operation.path, true);
2691
+ break;
2692
+ case "text":
2693
+ case "create":
2694
+ this.add(operation.path, false);
2695
+ break;
2696
+ }
2697
+ }
2698
+ return this.snapshots;
2699
+ }
2700
+ add(path2, includeChildren) {
2701
+ let candidate = path2;
2702
+ while (true) {
2703
+ const existing = this.snapshots.get(candidate);
2704
+ if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
2705
+ this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path2));
2706
+ }
2707
+ if (candidate === this.workspaceRoot)
2708
+ break;
2709
+ candidate = dirname6(candidate);
2710
+ }
2711
+ if (!includeChildren || !existsSync5(path2) || !lstatSync4(path2).isDirectory())
2712
+ return;
2713
+ for (const child of readdirSync2(path2))
2714
+ this.add(resolve5(path2, child), true);
2715
+ }
2716
+ }
2717
+ function snapshotOperations(operations, workspaceRoot) {
2718
+ return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
2719
+ }
2720
+
2721
+ class PlanPathIndex {
2722
+ firstChangeByPath = new Map;
2723
+ reportedPathByCanonical = new Map;
2724
+ build(operations) {
2725
+ for (const operation of operations) {
2726
+ switch (operation.kind) {
2727
+ case "rename":
2728
+ this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
2729
+ this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
2730
+ break;
2731
+ case "text":
2732
+ case "create":
2733
+ case "delete":
2734
+ this.add(operation.path, operation.reportedPath, operation.changeIndex);
2735
+ break;
2736
+ }
2737
+ }
2738
+ }
2739
+ add(path2, reportedPath2, changeIndex) {
2740
+ if (!this.firstChangeByPath.has(path2))
2741
+ this.firstChangeByPath.set(path2, changeIndex);
2742
+ if (!this.reportedPathByCanonical.has(path2))
2743
+ this.reportedPathByCanonical.set(path2, reportedPath2);
2744
+ }
2745
+ }
2746
+ function fingerprintWorkspaceEdit(edit, workspaceRoot) {
2747
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2748
+ if (!root.success)
2749
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2750
+ const parsed = parseWorkspaceEdit(edit, root.path);
2751
+ if (parsed.failures.length > 0)
2752
+ return { success: false, result: failureResult(parsed.failures) };
2753
+ return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
2754
+ }
2755
+ function planWorkspaceEdit(edit, workspaceRoot) {
2756
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2757
+ if (!root.success)
2758
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2759
+ const parsed = parseWorkspaceEdit(edit, root.path);
2760
+ if (parsed.failures.length > 0)
2761
+ return { success: false, result: failureResult(parsed.failures) };
2762
+ let snapshots;
2763
+ try {
2764
+ snapshots = snapshotOperations(parsed.operations, root.path);
2765
+ } catch (error) {
2766
+ return {
2767
+ success: false,
2768
+ result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
2769
+ };
2770
+ }
2771
+ const simulated = simulateOperations(parsed.operations, snapshots);
2772
+ if (simulated.failures.length > 0)
2773
+ return { success: false, result: failureResult(simulated.failures) };
2774
+ const paths = new PlanPathIndex;
2775
+ paths.build(parsed.operations);
2776
+ const plan = {
2777
+ workspaceRoot: root.path,
2778
+ operations: simulated.operations,
2779
+ snapshots,
2780
+ firstChangeByPath: paths.firstChangeByPath,
2781
+ reportedPathByCanonical: paths.reportedPathByCanonical,
2782
+ fingerprint: canonicalFingerprint(parsed.operations)
2783
+ };
2784
+ return { success: true, plan };
2785
+ }
2786
+ function failure(message, failedChange, base) {
2787
+ return {
2788
+ success: false,
2789
+ filesModified: base?.filesModified ?? [],
2790
+ totalEdits: base?.totalEdits ?? 0,
2791
+ errors: [message],
2792
+ ...failedChange === undefined ? {} : { failedChange },
2793
+ ...base?.lateAbort ? { lateAbort: true } : {}
2794
+ };
2795
+ }
2796
+ function responseFor(result) {
2797
+ if (result.success)
2798
+ return { applied: true };
2799
+ return {
2800
+ applied: false,
2801
+ failureReason: result.errors[0] ?? "workspace edit failed",
2802
+ ...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
2803
+ };
2804
+ }
2805
+ function isRecord4(value) {
2806
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2807
+ }
2808
+
2809
+ class WorkspaceMutationController {
2810
+ workspaceRoot;
2811
+ documents;
2812
+ activeLease = null;
2813
+ nextLeaseId = 1;
2814
+ io;
2815
+ constructor(workspaceRoot, documents) {
2816
+ this.workspaceRoot = workspaceRoot;
2817
+ this.documents = documents;
2818
+ }
2819
+ setIo(io) {
2820
+ this.io = io;
2821
+ }
2822
+ acquire(signal) {
2823
+ if (this.activeLease)
2824
+ return { success: false, result: failure("workspace mutation is already in progress") };
2825
+ if (signal?.aborted)
2826
+ return { success: false, result: failure("cancelled before mutating request") };
2827
+ const lease = {
2828
+ id: this.nextLeaseId,
2829
+ phase: "idle",
2830
+ ...signal === undefined ? {} : { signal }
2831
+ };
2832
+ this.nextLeaseId += 1;
2833
+ this.activeLease = lease;
2834
+ return { success: true, lease };
2835
+ }
2836
+ release(lease) {
2837
+ if (this.activeLease?.id !== lease.id)
2838
+ return;
2839
+ this.activeLease.phase = "sealed";
2840
+ this.activeLease = null;
2841
+ }
2842
+ isBeforeCommit(lease) {
2843
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
2844
+ }
2845
+ async handleApplyEdit(params) {
2846
+ const lease = this.activeLease;
2847
+ if (!lease)
2848
+ return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
2849
+ if (lease.phase !== "idle") {
2850
+ return {
2851
+ applied: false,
2852
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
2853
+ };
2854
+ }
2855
+ lease.phase = "applying";
2856
+ lease.applyCompletion = new Promise((resolve62) => {
2857
+ lease.resolveApply = resolve62;
2858
+ });
2859
+ const edit = isRecord4(params) ? params["edit"] : undefined;
2860
+ const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
2861
+ lease.serverApply = record2;
2862
+ lease.phase = "settled";
2863
+ lease.resolveApply?.();
2864
+ return responseFor(record2.result);
2865
+ }
2866
+ async reconcileRename(leaseToken, edit) {
2867
+ const lease = this.requireActiveLease(leaseToken);
2868
+ if (!lease)
2869
+ return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
2870
+ if (lease.phase === "applying")
2871
+ await lease.applyCompletion;
2872
+ if (lease.serverApply)
2873
+ return this.reconcileServerApply(lease.serverApply, edit);
2874
+ lease.phase = "sealed";
2875
+ if (!edit)
2876
+ return { edit, apply: failure("No edit provided") };
2877
+ const applied = await this.applyEdit(edit, lease);
2878
+ return { edit, apply: applied.result };
2879
+ }
2880
+ reconcileServerApply(record2, edit) {
2881
+ if (!edit)
2882
+ return { edit, apply: record2.result };
2883
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
2884
+ if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
2885
+ return { edit, apply: record2.result };
2886
+ }
2887
+ return {
2888
+ edit,
2889
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
2890
+ };
2891
+ }
2892
+ async applyEdit(edit, lease) {
2893
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
2894
+ if (!planned.success)
2895
+ return { fingerprint: null, result: planned.result };
2896
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
2897
+ if (versionFailure) {
2898
+ return {
2899
+ fingerprint: planned.plan.fingerprint,
2900
+ result: failure(versionFailure.message, versionFailure.changeIndex)
2901
+ };
2902
+ }
2903
+ const commit = commitWorkspaceEditPlan(planned.plan, {
2904
+ ...lease.signal === undefined ? {} : { signal: lease.signal },
2905
+ ...this.io === undefined ? {} : { io: this.io }
1056
2906
  });
2907
+ let result = commit.result;
2908
+ if (commit.delta.operations.length > 0) {
2909
+ try {
2910
+ await this.documents.synchronize(commit.delta);
2911
+ } catch (error) {
2912
+ const message = error instanceof Error ? error.message : String(error);
2913
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
2914
+ }
2915
+ }
2916
+ if (lease.signal?.aborted && !result.lateAbort)
2917
+ result = { ...result, lateAbort: true };
2918
+ return { fingerprint: planned.plan.fingerprint, result };
2919
+ }
2920
+ requireActiveLease(lease) {
2921
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
2922
+ }
2923
+ }
2924
+ var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
2925
+ var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
2926
+
2927
+ class LspClient extends LspClientConnection {
2928
+ diagnosticPullErrors = [];
2929
+ documents;
2930
+ workspaceMutations;
2931
+ diagnosticsFreshnessTimeoutMs;
2932
+ constructor(root, server2, options = {}) {
2933
+ super(root, server2, options);
2934
+ this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2935
+ this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2936
+ versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2937
+ });
2938
+ this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
2939
+ this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
2940
+ }
2941
+ getDiagnosticPullErrors() {
2942
+ return this.diagnosticPullErrors;
1057
2943
  }
1058
- async definition(filePath, line, character) {
1059
- const absPath = resolve(contextCwd(), filePath);
2944
+ async openFile(filePath) {
2945
+ const absPath = this.resolveWorkspacePath(filePath);
2946
+ await this.documents.openFile(absPath);
2947
+ }
2948
+ getOpenDocumentVersion(filePath) {
2949
+ return this.documents.getVersion(this.resolveWorkspacePath(filePath));
2950
+ }
2951
+ getStoredDiagnostics(uri) {
2952
+ return [...this.documents.getStoredDiagnostics(uri)];
2953
+ }
2954
+ setWorkspaceEditIo(io) {
2955
+ this.workspaceMutations.setIo(io);
2956
+ }
2957
+ handlePublishDiagnostics(params) {
2958
+ super.handlePublishDiagnostics(params);
2959
+ this.documents.recordPublishedDiagnostics(params);
2960
+ }
2961
+ async definition(filePath, line, character, signal) {
2962
+ const absPath = this.resolveWorkspacePath(filePath);
1060
2963
  await this.openFile(absPath);
2964
+ const options = signal === undefined ? {} : { signal };
1061
2965
  return this.sendRequest("textDocument/definition", {
1062
- textDocument: { uri: pathToFileURL2(absPath).href },
2966
+ textDocument: { uri: pathToFileURL3(absPath).href },
1063
2967
  position: { line: line - 1, character }
1064
- });
2968
+ }, options);
1065
2969
  }
1066
- async references(filePath, line, character, includeDeclaration = true) {
1067
- const absPath = resolve(contextCwd(), filePath);
2970
+ async references(filePath, line, character, includeDeclaration = true, signal) {
2971
+ const absPath = this.resolveWorkspacePath(filePath);
1068
2972
  await this.openFile(absPath);
2973
+ const options = signal === undefined ? {} : { signal };
1069
2974
  return this.sendRequest("textDocument/references", {
1070
- textDocument: { uri: pathToFileURL2(absPath).href },
2975
+ textDocument: { uri: pathToFileURL3(absPath).href },
1071
2976
  position: { line: line - 1, character },
1072
2977
  context: { includeDeclaration }
1073
- });
2978
+ }, options);
1074
2979
  }
1075
- async documentSymbols(filePath) {
1076
- const absPath = resolve(contextCwd(), filePath);
2980
+ async documentSymbols(filePath, signal) {
2981
+ const absPath = this.resolveWorkspacePath(filePath);
1077
2982
  await this.openFile(absPath);
2983
+ const options = signal === undefined ? {} : { signal };
1078
2984
  return this.sendRequest("textDocument/documentSymbol", {
1079
- textDocument: { uri: pathToFileURL2(absPath).href }
1080
- });
2985
+ textDocument: { uri: pathToFileURL3(absPath).href }
2986
+ }, options);
1081
2987
  }
1082
- async workspaceSymbols(query) {
1083
- return this.sendRequest("workspace/symbol", { query });
2988
+ async workspaceSymbols(query, signal) {
2989
+ const options = signal === undefined ? {} : { signal };
2990
+ return this.sendRequest("workspace/symbol", { query }, options);
1084
2991
  }
1085
2992
  isUnsupportedDiagnosticPullError(error) {
1086
2993
  if (!(error instanceof Error))
@@ -1090,42 +2997,173 @@ class LspClient extends LspClientConnection {
1090
2997
  return true;
1091
2998
  return /unsupported|not supported|method not found|unknown request/i.test(error.message);
1092
2999
  }
1093
- async diagnostics(filePath) {
1094
- const absPath = resolve(contextCwd(), filePath);
1095
- const uri = pathToFileURL2(absPath).href;
1096
- await this.openFile(absPath);
1097
- await new Promise((r) => setTimeout(r, POST_DIAGNOSTICS_WAIT_MS));
1098
- try {
1099
- const result = await this.sendRequest("textDocument/diagnostic", {
1100
- textDocument: { uri }
1101
- });
1102
- if (result.items) {
1103
- return { items: result.items };
3000
+ freshnessTimeout(absPath) {
3001
+ return {
3002
+ items: [],
3003
+ transientError: {
3004
+ kind: "freshness_timeout",
3005
+ message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
1104
3006
  }
1105
- } catch (error) {
1106
- if (!this.isUnsupportedDiagnosticPullError(error)) {
1107
- this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
3007
+ };
3008
+ }
3009
+ parseDiagnosticPullReport(value) {
3010
+ if (value.kind === "unchanged") {
3011
+ return {
3012
+ type: "unchanged",
3013
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
3014
+ };
3015
+ }
3016
+ return {
3017
+ type: "full",
3018
+ diagnostics: value.items ?? [],
3019
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
3020
+ };
3021
+ }
3022
+ async diagnostics(filePath, signal) {
3023
+ signal?.throwIfAborted();
3024
+ const absPath = this.resolveWorkspacePath(filePath);
3025
+ const uri = pathToFileURL3(absPath).href;
3026
+ await this.openFile(absPath);
3027
+ const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
3028
+ for (;; ) {
3029
+ signal?.throwIfAborted();
3030
+ const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
3031
+ if (!snapshot)
3032
+ return this.freshnessTimeout(absPath);
3033
+ const push = this.documents.resolvePushDiagnostics(snapshot);
3034
+ if (push.status === "ready")
3035
+ return { items: [...push.diagnostics] };
3036
+ let pushFallbackOnly = !this.isDiagnosticPullSupported();
3037
+ if (!pushFallbackOnly) {
3038
+ const cached = this.documents.getPullCache(snapshot);
3039
+ try {
3040
+ const remainingMs2 = deadlineAt - Date.now();
3041
+ if (remainingMs2 <= 0)
3042
+ return this.freshnessTimeout(absPath);
3043
+ const result = await this.sendRequest("textDocument/diagnostic", {
3044
+ textDocument: { uri },
3045
+ ...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
3046
+ }, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
3047
+ if (!this.documents.isCurrentSnapshot(snapshot))
3048
+ continue;
3049
+ const report = this.parseDiagnosticPullReport(result);
3050
+ if (report.type === "full") {
3051
+ this.documents.recordPullDiagnostics(snapshot, {
3052
+ kind: "full",
3053
+ diagnostics: report.diagnostics,
3054
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
3055
+ });
3056
+ return { items: [...report.diagnostics] };
3057
+ }
3058
+ if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
3059
+ return { items: [...cached.diagnostics] };
3060
+ }
3061
+ } catch (error) {
3062
+ if (this.isUnsupportedDiagnosticPullError(error)) {
3063
+ this.setDiagnosticPullSupported(false);
3064
+ pushFallbackOnly = true;
3065
+ } else if (error instanceof LspRequestTimeoutError) {
3066
+ pushFallbackOnly = true;
3067
+ } else {
3068
+ this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
3069
+ throw error;
3070
+ }
3071
+ }
1108
3072
  }
3073
+ if (!pushFallbackOnly)
3074
+ continue;
3075
+ const remainingMs = deadlineAt - Date.now();
3076
+ if (remainingMs <= 0)
3077
+ return this.freshnessTimeout(absPath);
3078
+ const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
3079
+ await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
1109
3080
  }
1110
- return { items: this.getStoredDiagnostics(uri) };
1111
3081
  }
1112
- async prepareRename(filePath, line, character) {
1113
- const absPath = resolve(contextCwd(), filePath);
3082
+ async prepareRename(filePath, line, character, signal) {
3083
+ const absPath = this.resolveWorkspacePath(filePath);
1114
3084
  await this.openFile(absPath);
3085
+ const options = signal === undefined ? {} : { signal };
1115
3086
  return this.sendRequest("textDocument/prepareRename", {
1116
- textDocument: { uri: pathToFileURL2(absPath).href },
3087
+ textDocument: { uri: pathToFileURL3(absPath).href },
1117
3088
  position: { line: line - 1, character }
1118
- });
3089
+ }, options);
1119
3090
  }
1120
- async rename(filePath, line, character, newName) {
1121
- const absPath = resolve(contextCwd(), filePath);
3091
+ async rename(filePath, line, character, newName, signal) {
3092
+ const absPath = this.resolveWorkspacePath(filePath);
1122
3093
  await this.openFile(absPath);
1123
- return this.sendRequest("textDocument/rename", {
1124
- textDocument: { uri: pathToFileURL2(absPath).href },
1125
- position: { line: line - 1, character },
1126
- newName
1127
- });
3094
+ const acquired = this.workspaceMutations.acquire(signal);
3095
+ if (!acquired.success)
3096
+ return { edit: null, apply: acquired.result };
3097
+ const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
3098
+ try {
3099
+ const renameParams = {
3100
+ textDocument: { uri: pathToFileURL3(absPath).href },
3101
+ position: { line: line - 1, character },
3102
+ newName
3103
+ };
3104
+ const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
3105
+ signal: preCommitSignal.signal
3106
+ });
3107
+ return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
3108
+ } finally {
3109
+ preCommitSignal?.dispose();
3110
+ this.workspaceMutations.release(acquired.lease);
3111
+ }
1128
3112
  }
3113
+ resolveWorkspacePath(filePath) {
3114
+ return resolve6(this.root, filePath);
3115
+ }
3116
+ }
3117
+ function waitForDiagnosticsActivity(wait, signal) {
3118
+ if (!signal)
3119
+ return wait;
3120
+ if (signal.aborted)
3121
+ return Promise.reject(abortError2(signal));
3122
+ return new Promise((resolve72, reject) => {
3123
+ const onAbort = () => {
3124
+ signal.removeEventListener("abort", onAbort);
3125
+ reject(abortError2(signal));
3126
+ };
3127
+ signal.addEventListener("abort", onAbort, { once: true });
3128
+ wait.then(() => {
3129
+ signal.removeEventListener("abort", onAbort);
3130
+ resolve72();
3131
+ }, (error) => {
3132
+ signal.removeEventListener("abort", onAbort);
3133
+ reject(error);
3134
+ });
3135
+ });
3136
+ }
3137
+ function createPreCommitAbortSignal(source, isBeforeCommit) {
3138
+ if (!source)
3139
+ return;
3140
+ const controller = new AbortController;
3141
+ const onAbort = () => {
3142
+ if (isBeforeCommit() && !controller.signal.aborted)
3143
+ controller.abort(preCommitAbortReason(source));
3144
+ };
3145
+ if (source.aborted)
3146
+ onAbort();
3147
+ else
3148
+ source.addEventListener("abort", onAbort, { once: true });
3149
+ return {
3150
+ signal: controller.signal,
3151
+ dispose: () => source.removeEventListener("abort", onAbort)
3152
+ };
3153
+ }
3154
+ function preCommitAbortReason(source) {
3155
+ const reason = source.reason;
3156
+ if (reason instanceof Error && reason.name !== "AbortError")
3157
+ return reason;
3158
+ return new Error("LSP request cancelled before workspace edit commit");
3159
+ }
3160
+ function abortError2(signal) {
3161
+ const reason = signal.reason;
3162
+ if (reason instanceof Error)
3163
+ return reason;
3164
+ const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
3165
+ error.name = "AbortError";
3166
+ return error;
1129
3167
  }
1130
3168
  function installProcessSignalCleanup(cleanup) {
1131
3169
  const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
@@ -1153,7 +3191,7 @@ async function stopClientBestEffort(client) {
1153
3191
  function awaitWithSignal(promise, signal) {
1154
3192
  if (!signal)
1155
3193
  return promise;
1156
- return new Promise((resolve22, reject) => {
3194
+ return new Promise((resolve72, reject) => {
1157
3195
  let settled = false;
1158
3196
  const onAbort = () => {
1159
3197
  if (settled)
@@ -1171,7 +3209,7 @@ function awaitWithSignal(promise, signal) {
1171
3209
  return;
1172
3210
  settled = true;
1173
3211
  signal.removeEventListener("abort", onAbort);
1174
- resolve22(value);
3212
+ resolve72(value);
1175
3213
  }, (err) => {
1176
3214
  if (settled)
1177
3215
  return;
@@ -1196,7 +3234,7 @@ class LspManager {
1196
3234
  this.idleTimeoutMs = options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
1197
3235
  this.initTimeoutMs = options.initTimeoutMs ?? INIT_TIMEOUT_MS;
1198
3236
  this.reaperIntervalMs = options.reaperIntervalMs ?? REAPER_INTERVAL_MS;
1199
- this.clientFactory = options.clientFactory ?? ((root, server) => new LspClient(root, server));
3237
+ this.clientFactory = options.clientFactory ?? ((root, server2) => new LspClient(root, server2));
1200
3238
  this.now = options.now ?? (() => Date.now());
1201
3239
  this.startReaper();
1202
3240
  this.signalDisposer = installProcessSignalCleanup(() => this.stopAll());
@@ -1234,12 +3272,12 @@ class LspManager {
1234
3272
  await stopClientBestEffort(managed.client);
1235
3273
  }
1236
3274
  }
1237
- async getClient(root, server, signal) {
3275
+ async getClient(root, server2, signal) {
1238
3276
  if (this.disposed) {
1239
3277
  throw new Error("LspManager has been disposed");
1240
3278
  }
1241
3279
  signal?.throwIfAborted();
1242
- const key = this.getKey(root, server.id);
3280
+ const key = this.getKey(root, server2.id);
1243
3281
  let managed = this.clients.get(key);
1244
3282
  if (managed) {
1245
3283
  const t = this.now();
@@ -1268,13 +3306,13 @@ class LspManager {
1268
3306
  if (!managed.client.isAlive()) {
1269
3307
  await stopClientBestEffort(managed.client);
1270
3308
  this.clients.delete(key);
1271
- return this.getClient(root, server, signal);
3309
+ return this.getClient(root, server2, signal);
1272
3310
  }
1273
3311
  managed.refCount++;
1274
3312
  managed.lastUsedAt = this.now();
1275
3313
  return managed.client;
1276
3314
  }
1277
- const client = this.clientFactory(root, server);
3315
+ const client = this.clientFactory(root, server2);
1278
3316
  const initStartedAt = this.now();
1279
3317
  const initPromise = (async () => {
1280
3318
  await client.start();
@@ -1330,13 +3368,13 @@ class LspManager {
1330
3368
  this.clients.delete(key);
1331
3369
  stopClientBestEffort(managed.client);
1332
3370
  }
1333
- warmupClient(root, server) {
3371
+ warmupClient(root, server2) {
1334
3372
  if (this.disposed)
1335
3373
  return;
1336
- const key = this.getKey(root, server.id);
3374
+ const key = this.getKey(root, server2.id);
1337
3375
  if (this.clients.has(key))
1338
3376
  return;
1339
- const client = this.clientFactory(root, server);
3377
+ const client = this.clientFactory(root, server2);
1340
3378
  const initStartedAt = this.now();
1341
3379
  const initPromise = (async () => {
1342
3380
  await client.start();
@@ -1416,230 +3454,15 @@ function getLspManager() {
1416
3454
  }
1417
3455
  return _defaultInstance;
1418
3456
  }
1419
- async function disposeDefaultLspManager() {
1420
- if (_defaultInstance) {
1421
- const m = _defaultInstance;
1422
- _defaultInstance = null;
1423
- await m.stopAll();
1424
- }
1425
- }
1426
- function isPlainRecord(value) {
1427
- return typeof value === "object" && value !== null && !Array.isArray(value);
1428
- }
1429
- function isProcessAlive(pid) {
1430
- if (!Number.isInteger(pid) || pid <= 0)
1431
- return false;
1432
- try {
1433
- process.kill(pid, 0);
1434
- return true;
1435
- } catch (error) {
1436
- return error.code === "EPERM";
1437
- }
1438
- }
1439
- function readLockPid(lockPath) {
1440
- try {
1441
- const pid = Number.parseInt(readFileSync2(lockPath, "utf8").trim(), 10);
1442
- return Number.isInteger(pid) ? pid : null;
1443
- } catch {
1444
- return null;
1445
- }
1446
- }
1447
- function tryAcquireLock(lockPath, ownerPid = process.pid) {
1448
- mkdirSync(dirname(lockPath), { recursive: true });
1449
- for (let attempt = 0;attempt < 2; attempt += 1) {
1450
- const handle = writeLockFile(lockPath, ownerPid);
1451
- if (handle)
1452
- return handle;
1453
- if (!reapStaleLock(lockPath))
1454
- return null;
1455
- }
1456
- return null;
1457
- }
1458
- function writeLockFile(lockPath, ownerPid) {
1459
- try {
1460
- const fd = openSync(lockPath, "wx");
1461
- writeSync(fd, `${ownerPid}
1462
- `);
1463
- closeSync(fd);
1464
- return { release: () => unlinkQuietly(lockPath) };
1465
- } catch (error) {
1466
- if (error.code === "EEXIST")
1467
- return null;
1468
- throw error;
1469
- }
1470
- }
1471
- function reapStaleLock(lockPath) {
1472
- const pid = readLockPid(lockPath);
1473
- if (pid !== null && isProcessAlive(pid))
1474
- return false;
1475
- unlinkQuietly(lockPath);
1476
- return true;
1477
- }
1478
- function unlinkQuietly(path) {
1479
- try {
1480
- unlinkSync(path);
1481
- } catch (error) {}
1482
- }
1483
- var PROBE_TIMEOUT_MS = 500;
1484
- var DEFAULT_READY_TIMEOUT_MS = 5000;
1485
- var DEFAULT_POLL_INTERVAL_MS = 100;
1486
- var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
1487
-
1488
- class DaemonUnreachableError extends Error {
1489
- constructor(socketPath) {
1490
- super(`LSP daemon did not become reachable at ${socketPath}`);
1491
- this.name = "DaemonUnreachableError";
1492
- }
1493
- }
1494
- async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
1495
- const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1496
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1497
- if (await deps.probe(paths.socket))
1498
- return;
1499
- const lock = deps.acquireLock(paths.lock);
1500
- if (!lock) {
1501
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
1502
- return;
1503
- }
1504
- try {
1505
- if (await deps.probe(paths.socket))
1506
- return;
1507
- deps.cleanupStaleSocket(paths.socket);
1508
- deps.spawnDaemon(paths);
1509
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
1510
- } finally {
1511
- lock.release();
1512
- }
1513
- }
1514
- async function waitUntilReachable(socketPath, deps, readyTimeoutMs, pollIntervalMs) {
1515
- const deadline = deps.now() + readyTimeoutMs;
1516
- for (;; ) {
1517
- if (await deps.probe(socketPath))
1518
- return;
1519
- if (deps.now() >= deadline)
1520
- throw new DaemonUnreachableError(socketPath);
1521
- await deps.sleep(pollIntervalMs);
1522
- }
1523
- }
1524
- function probeSocket(socketPath, timeoutMs = PROBE_TIMEOUT_MS) {
1525
- return new Promise((resolve22) => {
1526
- const socket = connect(socketPath);
1527
- const finish = (ok) => {
1528
- socket.destroy();
1529
- resolve22(ok);
1530
- };
1531
- const timer = setTimeout(() => finish(false), timeoutMs);
1532
- timer.unref?.();
1533
- socket.once("connect", () => {
1534
- clearTimeout(timer);
1535
- finish(true);
1536
- });
1537
- socket.once("error", () => {
1538
- clearTimeout(timer);
1539
- finish(false);
1540
- });
1541
- });
1542
- }
1543
- function spawnDaemonProcess(paths) {
1544
- mkdirSync2(dirname2(paths.log), { recursive: true });
1545
- const logFd = openSync2(paths.log, "a");
1546
- try {
1547
- const cliPath = resolveDaemonCliPath();
1548
- const child = spawn2(execPath, [cliPath, "daemon"], {
1549
- detached: true,
1550
- stdio: ["ignore", logFd, logFd]
1551
- });
1552
- child.unref();
1553
- } finally {
1554
- closeSync2(logFd);
1555
- }
1556
- }
1557
- function resolveDaemonCliPath(env = process.env) {
1558
- const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
1559
- if (override)
1560
- return override;
1561
- return fileURLToPath(new URL("./cli.js", import.meta.url));
1562
- }
1563
- function defaultEnsureDaemonDeps() {
1564
- return {
1565
- probe: (socketPath) => probeSocket(socketPath),
1566
- acquireLock: (lockPath) => tryAcquireLock(lockPath),
1567
- cleanupStaleSocket: (socketPath) => {
1568
- if (existsSync2(socketPath))
1569
- unlinkQuietly(socketPath);
1570
- },
1571
- spawnDaemon: (paths) => spawnDaemonProcess(paths),
1572
- sleep: (ms) => new Promise((resolve22) => {
1573
- setTimeout(resolve22, ms);
1574
- }),
1575
- now: () => Date.now()
1576
- };
1577
- }
1578
- var requireFromHere = createRequire(import.meta.url);
1579
- var MAX_SOCKET_PATH_LENGTH = 100;
1580
- var CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
1581
- function resolveDaemonVersion(requireFn = requireFromHere) {
1582
- for (const candidate of ["./package.json", "../package.json"]) {
1583
- try {
1584
- const pkg = requireFn(candidate);
1585
- if (typeof pkg.version === "string" && pkg.version.length > 0)
1586
- return pkg.version;
1587
- } catch {}
1588
- }
1589
- return "0";
1590
- }
1591
- function daemonBaseDir(env = process.env) {
1592
- const explicit = env["CODEX_LSP_DAEMON_DIR"]?.trim();
1593
- if (explicit)
1594
- return explicit;
1595
- const pluginData = env["PLUGIN_DATA"]?.trim();
1596
- if (pluginData)
1597
- return join2(pluginData, "daemon");
1598
- const codexHome = env["CODEX_HOME"]?.trim();
1599
- const home = codexHome && codexHome.length > 0 ? codexHome : join2(homedir(), ".codex");
1600
- return join2(home, "codex-lsp", "daemon");
1601
- }
1602
- function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
1603
- const dir = join2(daemonBaseDir(env), `v${version}`);
1604
- return {
1605
- version,
1606
- dir,
1607
- socket: resolveSocketPath(dir, version),
1608
- lock: join2(dir, "daemon.lock"),
1609
- pid: join2(dir, "daemon.pid"),
1610
- log: join2(dir, "daemon.log")
1611
- };
1612
- }
1613
- function resolveDaemonVersionFromEnv(env = process.env) {
1614
- const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
1615
- return version && version.length > 0 ? version : null;
1616
- }
1617
- function resolveSocketPath(dir, version) {
1618
- const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
1619
- if (process.platform === "win32") {
1620
- return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
1621
- }
1622
- const natural = join2(dir, "daemon.sock");
1623
- if (natural.length < MAX_SOCKET_PATH_LENGTH)
1624
- return natural;
1625
- return join2(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
1626
- }
1627
- var HEADER_SEPARATOR2 = Buffer.from(`\r
1628
- \r
1629
- `);
1630
- var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60000;
1631
3457
  function getInstallDecisionsPath() {
1632
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
1633
- if (!override)
1634
- return join3(homedir2(), ".codex", "lsp-install-decisions.json");
1635
- return isAbsolute(override) ? override : join3(homedir2(), override);
3458
+ return lspRequestContext().installDecisionsPath;
1636
3459
  }
1637
3460
  function loadInstallDecisions() {
1638
- const path = getInstallDecisionsPath();
1639
- if (!existsSync3(path))
3461
+ const path2 = getInstallDecisionsPath();
3462
+ if (!existsSync6(path2))
1640
3463
  return {};
1641
3464
  try {
1642
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
3465
+ const parsed = JSON.parse(readFileSync4(path2, "utf8"));
1643
3466
  return isInstallDecisions(parsed) ? parsed : {};
1644
3467
  } catch {
1645
3468
  return {};
@@ -1657,22 +3480,22 @@ function isInstallDecision(value) {
1657
3480
  return value === "declined" || value === "allowed";
1658
3481
  }
1659
3482
  function writeInstallDecisions(decisions) {
1660
- const path = getInstallDecisionsPath();
1661
- mkdirSync3(dirname3(path), { recursive: true });
1662
- const tmpPath = `${path}.tmp`;
1663
- writeFileSync(tmpPath, `${JSON.stringify(decisions, null, 2)}
3483
+ const path2 = getInstallDecisionsPath();
3484
+ mkdirSync3(dirname7(path2), { recursive: true });
3485
+ const tmpPath = `${path2}.tmp`;
3486
+ writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
1664
3487
  `, "utf8");
1665
- renameSync(tmpPath, path);
3488
+ renameSync2(tmpPath, path2);
1666
3489
  }
1667
3490
  function isInstallDecisions(value) {
1668
- return isRecord2(value) && Object.values(value).every(isInstallDecisionRecord);
3491
+ return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
1669
3492
  }
1670
3493
  function isInstallDecisionRecord(value) {
1671
- if (!isRecord2(value))
3494
+ if (!isRecord5(value))
1672
3495
  return false;
1673
3496
  return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
1674
3497
  }
1675
- function isRecord2(value) {
3498
+ function isRecord5(value) {
1676
3499
  return typeof value === "object" && value !== null && !Array.isArray(value);
1677
3500
  }
1678
3501
  var LSP_INSTALL_HINTS = {
@@ -1820,27 +3643,17 @@ var BUILTIN_SERVERS = {
1820
3643
  extensions: [".razor", ".cshtml"]
1821
3644
  }
1822
3645
  };
1823
- function resolveProjectConfigPath(path) {
1824
- return isAbsolute2(path) ? path : join4(contextCwd(), path);
1825
- }
1826
3646
  function getProjectConfigPaths() {
1827
- const projectOverride = contextEnv("LSP_TOOLS_MCP_PROJECT_CONFIG");
1828
- if (projectOverride) {
1829
- return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
1830
- }
1831
- return [join4(contextCwd(), ".codex", "lsp-client.json")];
3647
+ return lspRequestContext().projectConfigPaths;
1832
3648
  }
1833
3649
  function getUserConfigPath() {
1834
- const userOverride = contextEnv("LSP_TOOLS_MCP_USER_CONFIG");
1835
- if (!userOverride)
1836
- return join4(homedir3(), ".codex", "lsp-client.json");
1837
- return isAbsolute2(userOverride) ? userOverride : join4(homedir3(), userOverride);
3650
+ return lspRequestContext().userConfigPath;
1838
3651
  }
1839
- function loadJsonFile(path) {
1840
- if (!existsSync4(path))
3652
+ function loadJsonFile(path2) {
3653
+ if (!existsSync7(path2))
1841
3654
  return null;
1842
3655
  try {
1843
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
3656
+ const parsed = JSON.parse(readFileSync5(path2, "utf-8"));
1844
3657
  return isConfigJson(parsed) ? parsed : null;
1845
3658
  } catch {
1846
3659
  return null;
@@ -1857,8 +3670,8 @@ function loadAllConfigs() {
1857
3670
  return configs;
1858
3671
  }
1859
3672
  function loadFirstJsonFile(paths) {
1860
- for (const path of paths) {
1861
- const config = loadJsonFile(path);
3673
+ for (const path2 of paths) {
3674
+ const config = loadJsonFile(path2);
1862
3675
  if (config)
1863
3676
  return config;
1864
3677
  }
@@ -1979,16 +3792,16 @@ function applyOptionalServerFields(server2, entry) {
1979
3792
  }
1980
3793
  }
1981
3794
  function isConfigJson(value) {
1982
- if (!isRecord3(value))
3795
+ if (!isRecord6(value))
1983
3796
  return false;
1984
3797
  const lsp = value["lsp"];
1985
- return lsp === undefined || isRecord3(lsp);
3798
+ return lsp === undefined || isRecord6(lsp);
1986
3799
  }
1987
3800
  function parseLspEntry(value) {
1988
3801
  return isLspEntry(value) ? value : null;
1989
3802
  }
1990
3803
  function isLspEntry(value) {
1991
- if (!isRecord3(value))
3804
+ if (!isRecord6(value))
1992
3805
  return false;
1993
3806
  const disabled = value["disabled"];
1994
3807
  const command = value["command"];
@@ -1996,15 +3809,15 @@ function isLspEntry(value) {
1996
3809
  const priority = value["priority"];
1997
3810
  const env = value["env"];
1998
3811
  const initialization = value["initialization"];
1999
- 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));
3812
+ 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));
2000
3813
  }
2001
3814
  function isStringArray(value) {
2002
3815
  return Array.isArray(value) && value.every((item) => typeof item === "string");
2003
3816
  }
2004
3817
  function isStringRecord(value) {
2005
- return isRecord3(value) && Object.values(value).every((item) => typeof item === "string");
3818
+ return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
2006
3819
  }
2007
- function isRecord3(value) {
3820
+ function isRecord6(value) {
2008
3821
  return typeof value === "object" && value !== null && !Array.isArray(value);
2009
3822
  }
2010
3823
  function getDisabledServerIds() {
@@ -2030,7 +3843,7 @@ function isServerInstalled(command, _workingDirectory) {
2030
3843
  if (!cmd)
2031
3844
  return false;
2032
3845
  if (cmd.includes("/") || cmd.includes("\\")) {
2033
- if (existsSync5(cmd))
3846
+ if (existsSync8(cmd))
2034
3847
  return true;
2035
3848
  }
2036
3849
  const isWindows = process.platform === "win32";
@@ -2051,7 +3864,7 @@ function isServerInstalled(command, _workingDirectory) {
2051
3864
  const paths = pathEnv.split(delimiter3);
2052
3865
  for (const p of paths) {
2053
3866
  for (const suffix of exts) {
2054
- if (existsSync5(join5(p, cmd + suffix))) {
3867
+ if (existsSync8(join3(p, cmd + suffix))) {
2055
3868
  return true;
2056
3869
  }
2057
3870
  }
@@ -2146,39 +3959,50 @@ function getAllServers() {
2146
3959
  var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2147
3960
  function isDirectoryPath(filePath) {
2148
3961
  try {
2149
- return statSync2(filePath).isDirectory();
3962
+ return statSync4(filePath).isDirectory();
2150
3963
  } catch {
2151
3964
  return false;
2152
3965
  }
2153
3966
  }
2154
3967
  function findWorkspaceRoot(filePath) {
2155
- const abs = resolve2(contextCwd(), filePath);
3968
+ const abs = resolvePathInsideContext(filePath);
2156
3969
  let dir = abs;
2157
3970
  if (!isDirectoryPath(dir)) {
2158
- dir = dirname4(dir);
3971
+ dir = dirname8(dir);
2159
3972
  }
2160
3973
  let prevDir = "";
2161
3974
  while (dir !== prevDir) {
2162
3975
  for (const marker of WORKSPACE_MARKERS) {
2163
- if (existsSync6(join6(dir, marker))) {
3976
+ if (existsSync9(join4(dir, marker))) {
2164
3977
  return dir;
2165
3978
  }
2166
3979
  }
2167
3980
  prevDir = dir;
2168
- dir = dirname4(dir);
3981
+ dir = dirname8(dir);
2169
3982
  }
2170
- return dirname4(abs);
3983
+ return dirname8(abs);
3984
+ }
3985
+ function resolvePathInsideContext(filePath) {
3986
+ const cwd = contextCwd();
3987
+ const abs = resolve7(cwd, filePath);
3988
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
3989
+ if (!isPathInside(cwd, canonical)) {
3990
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
3991
+ }
3992
+ return canonical;
2171
3993
  }
2172
3994
  function formatServerLookupError(result) {
2173
3995
  if (result.status === "not_installed") {
2174
3996
  return formatNotInstalled(result);
2175
3997
  }
3998
+ const context = lspRequestContext();
3999
+ const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
2176
4000
  return [
2177
4001
  `No LSP server configured for extension: ${result.extension}`,
2178
4002
  "",
2179
4003
  `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
2180
4004
  "",
2181
- "Configure a custom server in '.codex/lsp-client.json':",
4005
+ `Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
2182
4006
  " {",
2183
4007
  ' "lsp": {',
2184
4008
  ' "my-server": {',
@@ -2194,6 +4018,7 @@ function formatNotInstalled(result) {
2194
4018
  const { server: server2, installHint } = result;
2195
4019
  const extensions = server2.extensions.join(", ");
2196
4020
  const decision = loadInstallDecision(server2.id)?.decision;
4021
+ const context = lspRequestContext();
2197
4022
  if (decision === "declined") {
2198
4023
  return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
2199
4024
  }
@@ -2209,6 +4034,17 @@ function formatNotInstalled(result) {
2209
4034
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
2210
4035
  ` ${installHint}`
2211
4036
  ].join(`
4037
+ `);
4038
+ }
4039
+ if (!context.capabilities.installDecisionTool) {
4040
+ return [
4041
+ ...header,
4042
+ "To install, run:",
4043
+ ` ${installHint}`,
4044
+ "",
4045
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
4046
+ "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
4047
+ ].join(`
2212
4048
  `);
2213
4049
  }
2214
4050
  return [
@@ -2220,6 +4056,7 @@ function formatNotInstalled(result) {
2220
4056
  "- If the user agrees: run the install command above, then retry this tool.",
2221
4057
  "- If the user declines, OR has NOT explicitly asked for LSP installation:",
2222
4058
  ` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
4059
+ ` which writes to ${context.installDecisionsPath},`,
2223
4060
  " then ignore this message and proceed WITHOUT LSP."
2224
4061
  ].join(`
2225
4062
  `);
@@ -2233,14 +4070,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
2233
4070
  "prepareRename"
2234
4071
  ]);
2235
4072
  async function withLspClient(filePath, fn, toolName, options = {}) {
2236
- const absPath = resolve2(contextCwd(), filePath);
4073
+ const absPath = resolvePathInsideContext(filePath);
2237
4074
  if (isDirectoryPath(absPath)) {
2238
4075
  throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
2239
4076
  }
2240
4077
  const ext = effectiveExtension(absPath);
2241
4078
  const result = findServerForExtension(ext);
2242
4079
  if (result.status !== "found") {
2243
- throw new LspServerLookupError(formatServerLookupError(result));
4080
+ throw new LspServerLookupError(formatServerLookupError(result), result);
2244
4081
  }
2245
4082
  const server2 = result.server;
2246
4083
  const root = findWorkspaceRoot(absPath);
@@ -2273,7 +4110,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
2273
4110
  hint: 4
2274
4111
  };
2275
4112
  function uriToPath(uri) {
2276
- return fileURLToPath2(uri);
4113
+ return fileURLToPath3(uri);
2277
4114
  }
2278
4115
  function formatLocation(loc) {
2279
4116
  if ("targetUri" in loc) {
@@ -2359,6 +4196,9 @@ function formatApplyResult(result) {
2359
4196
  for (const file of result.filesModified) {
2360
4197
  lines.push(` - ${file}`);
2361
4198
  }
4199
+ if (result.lateAbort) {
4200
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
4201
+ }
2362
4202
  } else {
2363
4203
  lines.push("Failed to apply some changes:");
2364
4204
  for (const err of result.errors) {
@@ -2372,6 +4212,7 @@ function formatApplyResult(result) {
2372
4212
  `);
2373
4213
  }
2374
4214
  var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
4215
+ var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
2375
4216
  function collectFilesWithExtension(dir, extension, maxFiles) {
2376
4217
  const files = [];
2377
4218
  function walk(currentDir) {
@@ -2379,17 +4220,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2379
4220
  return;
2380
4221
  let entries = [];
2381
4222
  try {
2382
- entries = readdirSync(currentDir);
4223
+ entries = readdirSync3(currentDir);
2383
4224
  } catch {
2384
4225
  return;
2385
4226
  }
2386
4227
  for (const entry of entries) {
2387
4228
  if (files.length >= maxFiles)
2388
4229
  return;
2389
- const fullPath = join7(currentDir, entry);
4230
+ const fullPath = join5(currentDir, entry);
2390
4231
  let stat;
2391
4232
  try {
2392
- stat = lstatSync(fullPath);
4233
+ stat = lstatSync5(fullPath);
2393
4234
  } catch {
2394
4235
  continue;
2395
4236
  }
@@ -2407,52 +4248,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2407
4248
  walk(dir);
2408
4249
  return files;
2409
4250
  }
2410
- async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
4251
+ async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
2411
4252
  if (!extension.startsWith(".")) {
2412
4253
  throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
2413
4254
  }
2414
- const absDir = resolve3(contextCwd(), directory);
2415
- if (!existsSync7(absDir)) {
4255
+ const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
4256
+ if (!existsSync10(absDir)) {
2416
4257
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
2417
4258
  }
2418
- const serverResult = findServerForExtension(extension);
4259
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
2419
4260
  if (serverResult.status !== "found") {
2420
4261
  throw new LspServerLookupError(formatServerLookupError(serverResult));
2421
4262
  }
2422
4263
  const server2 = serverResult.server;
2423
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
4264
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
2424
4265
  const wasCapped = allFiles.length > maxFiles;
2425
4266
  const filesToProcess = allFiles.slice(0, maxFiles);
2426
4267
  if (filesToProcess.length === 0) {
2427
- return [
4268
+ const output = [
2428
4269
  `Directory: ${absDir}`,
2429
4270
  `Extension: ${extension}`,
2430
4271
  "Files scanned: 0",
2431
4272
  `No files found with extension "${extension}".`
2432
4273
  ].join(`
2433
4274
  `);
4275
+ return { output, totalDiagnostics: 0, fileFailures: [] };
2434
4276
  }
2435
- const root = findWorkspaceRoot(absDir);
2436
- const manager = getLspManager();
4277
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
4278
+ const manager = options.manager ?? getLspManager();
2437
4279
  const allDiagnostics = [];
2438
4280
  const fileErrors = [];
4281
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
4282
+ options.signal?.throwIfAborted();
2439
4283
  const client = await manager.getClient(root, server2);
2440
4284
  try {
2441
- for (const file of filesToProcess) {
2442
- try {
2443
- const result = await client.diagnostics(file);
2444
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
2445
- allDiagnostics.push(...filtered.map((diagnostic) => ({
2446
- filePath: file,
2447
- diagnostic
2448
- })));
2449
- } catch (e) {
2450
- fileErrors.push({
2451
- file,
2452
- error: e instanceof Error ? e.message : String(e)
2453
- });
4285
+ let nextIndex = 0;
4286
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
4287
+ for (;; ) {
4288
+ if (options.signal?.aborted)
4289
+ return;
4290
+ const file = filesToProcess[nextIndex];
4291
+ nextIndex += 1;
4292
+ if (file === undefined)
4293
+ return;
4294
+ try {
4295
+ const result = await client.diagnostics(file, options.signal);
4296
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
4297
+ allDiagnostics.push(...filtered.map((diagnostic) => ({
4298
+ filePath: file,
4299
+ diagnostic
4300
+ })));
4301
+ } catch (e) {
4302
+ fileErrors.push({
4303
+ file,
4304
+ error: e instanceof Error ? e.message : String(e)
4305
+ });
4306
+ }
2454
4307
  }
2455
- }
4308
+ });
4309
+ await Promise.all(workers);
2456
4310
  } finally {
2457
4311
  manager.releaseClient(root, server2.id);
2458
4312
  }
@@ -2480,8 +4334,8 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
2480
4334
  lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
2481
4335
  }
2482
4336
  }
2483
- return lines.join(`
2484
- `);
4337
+ return { output: lines.join(`
4338
+ `), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
2485
4339
  }
2486
4340
  var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
2487
4341
  var MAX_SCAN_ENTRIES = 500;
@@ -2493,17 +4347,17 @@ function inferExtensionFromDirectory(directory) {
2493
4347
  return;
2494
4348
  let entries;
2495
4349
  try {
2496
- entries = readdirSync2(dir);
4350
+ entries = readdirSync4(dir);
2497
4351
  } catch {
2498
4352
  return;
2499
4353
  }
2500
4354
  for (const entry of entries) {
2501
4355
  if (scanned >= MAX_SCAN_ENTRIES)
2502
4356
  return;
2503
- const fullPath = join8(dir, entry);
4357
+ const fullPath = join6(dir, entry);
2504
4358
  let stat;
2505
4359
  try {
2506
- stat = lstatSync2(fullPath);
4360
+ stat = lstatSync6(fullPath);
2507
4361
  } catch {
2508
4362
  continue;
2509
4363
  }
@@ -2574,10 +4428,45 @@ function missingDependencyResult(error, details) {
2574
4428
  details: {
2575
4429
  ...details,
2576
4430
  error: message,
2577
- errorKind: "missing_dependency"
4431
+ errorKind: "missing_dependency",
4432
+ ...availabilityDetails(error)
2578
4433
  }
2579
4434
  };
2580
4435
  }
4436
+ function availabilityDetails(error) {
4437
+ const availability = missingDependencyAvailability(error);
4438
+ return availability === null ? {} : { availability };
4439
+ }
4440
+ function missingDependencyAvailability(error) {
4441
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
4442
+ return null;
4443
+ const context = lspRequestContext();
4444
+ switch (error.lookup.status) {
4445
+ case "not_configured":
4446
+ return {
4447
+ kind: "not_configured",
4448
+ extension: error.lookup.extension,
4449
+ availableServers: [...error.lookup.availableServers],
4450
+ projectConfigPaths: [...context.projectConfigPaths],
4451
+ userConfigPath: context.userConfigPath,
4452
+ installDecisionTool: context.capabilities.installDecisionTool
4453
+ };
4454
+ case "not_installed":
4455
+ return {
4456
+ kind: "not_installed",
4457
+ serverId: error.lookup.server.id,
4458
+ command: [...error.lookup.server.command],
4459
+ extensions: [...error.lookup.server.extensions],
4460
+ installHint: error.lookup.installHint,
4461
+ installDecisionTool: context.capabilities.installDecisionTool,
4462
+ installDecisionsPath: context.installDecisionsPath
4463
+ };
4464
+ default: {
4465
+ const exhaustive = error.lookup;
4466
+ return exhaustive;
4467
+ }
4468
+ }
4469
+ }
2581
4470
  function requireString(params, key) {
2582
4471
  const value = params[key];
2583
4472
  if (typeof value !== "string" || value.length === 0) {
@@ -2628,7 +4517,7 @@ async function executeLspDiagnostics(params, signal) {
2628
4517
  const filePath = requireString(params, "filePath");
2629
4518
  const severity = severityFilter(params);
2630
4519
  try {
2631
- const absPath = resolve4(contextCwd(), filePath);
4520
+ const absPath = resolvePathInsideContext(filePath);
2632
4521
  if (isDirectoryPath(absPath)) {
2633
4522
  const extension = inferExtensionFromDirectory(absPath);
2634
4523
  if (!extension) {
@@ -2645,18 +4534,33 @@ async function executeLspDiagnostics(params, signal) {
2645
4534
  };
2646
4535
  return text(message, details3);
2647
4536
  }
2648
- const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
4537
+ const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
2649
4538
  const details2 = {
2650
4539
  filePath,
2651
4540
  severity,
2652
4541
  mode: "directory",
2653
4542
  diagnostics: [],
4543
+ totalDiagnostics: output2.totalDiagnostics,
4544
+ truncated: false,
4545
+ fileFailures: [...output2.fileFailures]
4546
+ };
4547
+ return text(output2.output, details2);
4548
+ }
4549
+ const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
4550
+ if (result.transientError) {
4551
+ const message = result.transientError.message;
4552
+ const details2 = {
4553
+ filePath,
4554
+ severity,
4555
+ mode: "file",
4556
+ diagnostics: [],
2654
4557
  totalDiagnostics: 0,
2655
- truncated: false
4558
+ truncated: false,
4559
+ error: message,
4560
+ errorKind: result.transientError.kind
2656
4561
  };
2657
- return text(output2, details2);
4562
+ return text(message, details2, true);
2658
4563
  }
2659
- const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
2660
4564
  const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
2661
4565
  const total = diagnostics.length;
2662
4566
  const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
@@ -2714,7 +4618,7 @@ async function executeLspGotoDefinition(params, signal) {
2714
4618
  const line = requireNumber(params, "line");
2715
4619
  const character = requireNumber(params, "character");
2716
4620
  try {
2717
- const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
4621
+ const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
2718
4622
  const locations = !result ? [] : Array.isArray(result) ? result : [result];
2719
4623
  const details = { filePath, line, character, locations };
2720
4624
  if (locations.length === 0)
@@ -2733,216 +4637,53 @@ async function executeLspGotoDefinition(params, signal) {
2733
4637
  throw error;
2734
4638
  }
2735
4639
  }
2736
- async function executeLspFindReferences(params, signal) {
2737
- const filePath = requireString(params, "filePath");
2738
- const line = requireNumber(params, "line");
2739
- const character = requireNumber(params, "character");
2740
- const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
2741
- try {
2742
- const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
2743
- const references = Array.isArray(result) ? result : [];
2744
- const total = references.length;
2745
- const truncated = total > DEFAULT_MAX_REFERENCES;
2746
- const limited = truncated ? references.slice(0, DEFAULT_MAX_REFERENCES) : references;
2747
- const details = {
2748
- filePath,
2749
- line,
2750
- character,
2751
- references,
2752
- totalReferences: total,
2753
- truncated
2754
- };
2755
- if (total === 0)
2756
- return text("No references found", details);
2757
- const output = [
2758
- ...truncated ? [`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`] : [],
2759
- ...limited.map(formatLocation)
2760
- ].join(`
2761
- `);
2762
- return text(output, details);
2763
- } catch (error) {
2764
- const missingDependency = missingDependencyResult(error, {
2765
- filePath,
2766
- line,
2767
- character,
2768
- references: [],
2769
- totalReferences: 0,
2770
- truncated: false
2771
- });
2772
- if (missingDependency)
2773
- return missingDependency;
2774
- throw error;
2775
- }
2776
- }
2777
- function errorMessage2(error) {
2778
- return error instanceof Error ? error.message : String(error);
2779
- }
2780
- function isPathInsideWorkspace(filePath, workspaceRoot) {
2781
- const relativePath = relative(workspaceRoot, filePath);
2782
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
2783
- }
2784
- function realpathForValidation(filePath) {
2785
- if (existsSync8(filePath))
2786
- return realpathSync(filePath);
2787
- const parent = dirname5(filePath);
2788
- return resolve5(realpathSync(parent), relative(parent, filePath));
2789
- }
2790
- function uriToWorkspacePath(uri, workspaceRoot) {
2791
- let filePath;
2792
- try {
2793
- filePath = fileURLToPath3(uri);
2794
- } catch (error) {
2795
- return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
2796
- }
2797
- let validatedPath;
2798
- try {
2799
- validatedPath = realpathForValidation(filePath);
2800
- } catch (error) {
2801
- return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
2802
- }
2803
- if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
2804
- return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
2805
- }
2806
- return { success: true, path: filePath };
2807
- }
2808
- function applyTextEditsToFile(filePath, edits) {
2809
- try {
2810
- const content = readFileSync5(filePath, "utf-8");
2811
- const lines = content.split(`
2812
- `);
2813
- const sortedEdits = [...edits].sort((a, b) => {
2814
- if (b.range.start.line !== a.range.start.line) {
2815
- return b.range.start.line - a.range.start.line;
2816
- }
2817
- return b.range.start.character - a.range.start.character;
2818
- });
2819
- for (const edit of sortedEdits) {
2820
- const startLine = edit.range.start.line;
2821
- const startChar = edit.range.start.character;
2822
- const endLine = edit.range.end.line;
2823
- const endChar = edit.range.end.character;
2824
- if (startLine === endLine) {
2825
- const line = lines[startLine] ?? "";
2826
- lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
2827
- } else {
2828
- const firstLine = lines[startLine] ?? "";
2829
- const lastLine = lines[endLine] ?? "";
2830
- const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
2831
- lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
2832
- `));
2833
- }
2834
- }
2835
- writeFileSync2(filePath, lines.join(`
2836
- `), "utf-8");
2837
- return { success: true, editCount: edits.length };
2838
- } catch (err) {
2839
- return {
2840
- success: false,
2841
- editCount: 0,
2842
- error: err instanceof Error ? err.message : String(err)
2843
- };
2844
- }
2845
- }
2846
- function applyWorkspaceEdit(edit, options = {}) {
2847
- if (!edit) {
2848
- return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
2849
- }
2850
- const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
2851
- const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
2852
- if (edit.changes) {
2853
- for (const [uri, edits] of Object.entries(edit.changes)) {
2854
- const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
2855
- if (!validatedPath.success) {
2856
- result.success = false;
2857
- result.errors.push(validatedPath.error);
2858
- continue;
2859
- }
2860
- const applyResult = applyTextEditsToFile(validatedPath.path, edits);
2861
- if (applyResult.success) {
2862
- result.filesModified.push(validatedPath.path);
2863
- result.totalEdits += applyResult.editCount;
2864
- } else {
2865
- result.success = false;
2866
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2867
- }
2868
- }
2869
- }
2870
- if (edit.documentChanges) {
2871
- for (const change of edit.documentChanges) {
2872
- if (!("kind" in change)) {
2873
- const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
2874
- if (!validatedPath.success) {
2875
- result.success = false;
2876
- result.errors.push(validatedPath.error);
2877
- continue;
2878
- }
2879
- const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
2880
- if (applyResult.success) {
2881
- result.filesModified.push(validatedPath.path);
2882
- result.totalEdits += applyResult.editCount;
2883
- } else {
2884
- result.success = false;
2885
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2886
- }
2887
- continue;
2888
- }
2889
- if (change.kind === "create") {
2890
- try {
2891
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2892
- if (!validatedPath.success) {
2893
- result.success = false;
2894
- result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
2895
- continue;
2896
- }
2897
- writeFileSync2(validatedPath.path, "", "utf-8");
2898
- result.filesModified.push(validatedPath.path);
2899
- } catch (err) {
2900
- result.success = false;
2901
- result.errors.push(`Create ${change.uri}: ${String(err)}`);
2902
- }
2903
- } else if (change.kind === "rename") {
2904
- try {
2905
- const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
2906
- const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
2907
- if (!oldPath.success || !newPath.success) {
2908
- const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
2909
- result.success = false;
2910
- result.errors.push(`Rename ${change.oldUri}: ${error}`);
2911
- continue;
2912
- }
2913
- const content = readFileSync5(oldPath.path, "utf-8");
2914
- writeFileSync2(newPath.path, content, "utf-8");
2915
- unlinkSync2(oldPath.path);
2916
- result.filesModified.push(newPath.path);
2917
- } catch (err) {
2918
- result.success = false;
2919
- result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
2920
- }
2921
- } else if (change.kind === "delete") {
2922
- try {
2923
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2924
- if (!validatedPath.success) {
2925
- result.success = false;
2926
- result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
2927
- continue;
2928
- }
2929
- unlinkSync2(validatedPath.path);
2930
- result.filesModified.push(validatedPath.path);
2931
- } catch (err) {
2932
- result.success = false;
2933
- result.errors.push(`Delete ${change.uri}: ${String(err)}`);
2934
- }
2935
- }
2936
- }
4640
+ async function executeLspFindReferences(params, signal) {
4641
+ const filePath = requireString(params, "filePath");
4642
+ const line = requireNumber(params, "line");
4643
+ const character = requireNumber(params, "character");
4644
+ const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
4645
+ try {
4646
+ const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
4647
+ const references = Array.isArray(result) ? result : [];
4648
+ const total = references.length;
4649
+ const truncated = total > DEFAULT_MAX_REFERENCES;
4650
+ const limited = truncated ? references.slice(0, DEFAULT_MAX_REFERENCES) : references;
4651
+ const details = {
4652
+ filePath,
4653
+ line,
4654
+ character,
4655
+ references,
4656
+ totalReferences: total,
4657
+ truncated
4658
+ };
4659
+ if (total === 0)
4660
+ return text("No references found", details);
4661
+ const output = [
4662
+ ...truncated ? [`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`] : [],
4663
+ ...limited.map(formatLocation)
4664
+ ].join(`
4665
+ `);
4666
+ return text(output, details);
4667
+ } catch (error) {
4668
+ const missingDependency = missingDependencyResult(error, {
4669
+ filePath,
4670
+ line,
4671
+ character,
4672
+ references: [],
4673
+ totalReferences: 0,
4674
+ truncated: false
4675
+ });
4676
+ if (missingDependency)
4677
+ return missingDependency;
4678
+ throw error;
2937
4679
  }
2938
- return result;
2939
4680
  }
2940
4681
  async function executeLspPrepareRename(params, signal) {
2941
4682
  const filePath = requireString(params, "filePath");
2942
4683
  const line = requireNumber(params, "line");
2943
4684
  const character = requireNumber(params, "character");
2944
4685
  try {
2945
- const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
4686
+ const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
2946
4687
  const details = { filePath, line, character, result };
2947
4688
  return text(formatPrepareRenameResult(result), details);
2948
4689
  } catch (error) {
@@ -2963,13 +4704,9 @@ async function executeLspRename(params, signal) {
2963
4704
  const character = requireNumber(params, "character");
2964
4705
  const newName = requireString(params, "newName");
2965
4706
  try {
2966
- const edit = await withLspClient(filePath, async (client, workspaceRoot) => ({
2967
- edit: await client.rename(filePath, line, character, newName),
2968
- workspaceRoot
2969
- }), "rename", clientOptions(signal));
2970
- const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
2971
- const details = { filePath, line, character, newName, apply, edit: edit.edit };
2972
- return text(formatApplyResult(apply), details, !apply.success);
4707
+ const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
4708
+ const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
4709
+ return text(formatApplyResult(result.apply), details, !result.apply.success);
2973
4710
  } catch (error) {
2974
4711
  const missingDependency = missingDependencyResult(error, {
2975
4712
  filePath,
@@ -3038,10 +4775,10 @@ async function executeLspSymbols(params, signal) {
3038
4775
  errorKind: "missing_query"
3039
4776
  });
3040
4777
  }
3041
- const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
4778
+ const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
3042
4779
  return formatSymbolsResult(filePath, scope, symbols2, limit, query);
3043
4780
  }
3044
- const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
4781
+ const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
3045
4782
  return formatSymbolsResult(filePath, scope, symbols, limit);
3046
4783
  } catch (error) {
3047
4784
  const query = optionalString(params, "query");
@@ -3195,35 +4932,8 @@ var LSP_MCP_TOOLS = [
3195
4932
  }
3196
4933
  ];
3197
4934
  var CONTEXT_KEY = "_context";
3198
- function encodeJsonLine(message) {
3199
- return `${JSON.stringify(message)}
3200
- `;
3201
- }
3202
- function createLineDecoder(onMessage, onParseError) {
3203
- let buffer = "";
3204
- return {
3205
- push(chunk) {
3206
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
3207
- let index = buffer.indexOf(`
3208
- `);
3209
- while (index !== -1) {
3210
- const raw = buffer.slice(0, index).trim();
3211
- buffer = buffer.slice(index + 1);
3212
- if (raw.length > 0) {
3213
- try {
3214
- onMessage(JSON.parse(raw));
3215
- } catch (error) {
3216
- onParseError?.(raw, error);
3217
- }
3218
- }
3219
- index = buffer.indexOf(`
3220
- `);
3221
- }
3222
- }
3223
- };
3224
- }
3225
4935
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
3226
- var REQUEST_ID = 1;
4936
+ var nextProxyRequestId = 1;
3227
4937
 
3228
4938
  class DaemonRequestError extends Error {
3229
4939
  requestWritten;
@@ -3233,44 +4943,59 @@ class DaemonRequestError extends Error {
3233
4943
  this.requestWritten = requestWritten;
3234
4944
  }
3235
4945
  }
3236
- async function callToolViaDaemon(name, args, options = {}) {
4946
+
4947
+ class DaemonAuthenticationRejectedError extends DaemonRequestError {
4948
+ constructor() {
4949
+ super("daemon authentication failed before dispatch", true);
4950
+ this.name = "DaemonAuthenticationRejectedError";
4951
+ }
4952
+ }
4953
+
4954
+ class DaemonRequestCancelledError extends DaemonRequestError {
4955
+ constructor(requestWritten) {
4956
+ super("daemon request cancelled", requestWritten);
4957
+ this.name = "DaemonRequestCancelledError";
4958
+ }
4959
+ }
4960
+ async function callToolViaDaemon(name, args, options) {
4961
+ const context = requireContext(options.context);
3237
4962
  const paths = options.paths ?? daemonPaths();
3238
4963
  const ensure = options.ensure ?? ensureDaemonRunning;
3239
4964
  const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
3240
- const requestArgs = withContext(args, options.context);
4965
+ const requestArgs = withContext(args, context);
3241
4966
  let lastError;
3242
- for (let attempt = 0;attempt < 2; attempt += 1) {
4967
+ let authRefreshUsed = false;
4968
+ for (let attempt = 0;attempt < 3; attempt += 1) {
3243
4969
  try {
3244
4970
  await ensure(paths);
3245
- return await sendToolCall(paths.socket, name, requestArgs, timeoutMs);
4971
+ const token = readAuthToken(paths);
4972
+ if (!token)
4973
+ throw new DaemonRequestError("daemon auth token missing", false);
4974
+ const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
4975
+ return await sendToolCall(paths, token, name, requestArgs, sendOptions);
3246
4976
  } catch (error) {
3247
4977
  lastError = error;
3248
- if (error instanceof DaemonRequestError && error.requestWritten)
4978
+ if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
4979
+ authRefreshUsed = true;
4980
+ continue;
4981
+ }
4982
+ if (error instanceof DaemonRequestCancelledError)
4983
+ break;
4984
+ if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
3249
4985
  break;
3250
4986
  }
3251
4987
  }
3252
4988
  return daemonUnreachableResult(paths, lastError);
3253
4989
  }
3254
- function callDiagnosticsViaDaemon(filePath, options = {}) {
4990
+ function callDiagnosticsViaDaemon(filePath, options) {
3255
4991
  return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
3256
4992
  }
3257
- var FORWARDED_ENV_KEYS = [
3258
- "LSP_TOOLS_MCP_PROJECT_CONFIG",
3259
- "LSP_TOOLS_MCP_USER_CONFIG",
3260
- "LSP_TOOLS_MCP_INSTALL_DECISIONS"
3261
- ];
3262
- function currentRequestContext(env = process.env) {
3263
- const forwarded = {};
3264
- for (const key of FORWARDED_ENV_KEYS) {
3265
- const value = env[key];
3266
- if (value !== undefined)
3267
- forwarded[key] = value;
3268
- }
3269
- return { cwd: process.cwd(), env: forwarded };
4993
+ function requireContext(context) {
4994
+ if (!context)
4995
+ throw new DaemonRequestError("daemon tool context is required", false);
4996
+ return parseLspRequestContext(context);
3270
4997
  }
3271
4998
  function withContext(args, context) {
3272
- if (!context || context.cwd === undefined && context.env === undefined)
3273
- return args;
3274
4999
  return { ...args, [CONTEXT_KEY]: context };
3275
5000
  }
3276
5001
  function daemonUnreachableResult(paths, error) {
@@ -3284,39 +5009,84 @@ function daemonUnreachableResult(paths, error) {
3284
5009
  `);
3285
5010
  return { content: [{ type: "text", text: text2 }], isError: true };
3286
5011
  }
3287
- function sendToolCall(socketPath, name, args, timeoutMs) {
3288
- return new Promise((resolve6, reject) => {
3289
- const socket = connect2(socketPath);
5012
+ function sendToolCall(paths, token, name, args, options) {
5013
+ return new Promise((resolve9, reject) => {
5014
+ const socket = connect2(paths.socket);
5015
+ const requestId = allocateProxyRequestId();
3290
5016
  let settled = false;
3291
5017
  let requestWritten = false;
5018
+ let cancelAfterWrite = false;
5019
+ const cancelPayload = () => encodeJsonLine({
5020
+ jsonrpc: "2.0",
5021
+ method: "$/cancelRequest",
5022
+ params: { _omo: authEnvelope(token), id: requestId }
5023
+ });
3292
5024
  const finish = (run) => {
3293
5025
  if (settled)
3294
5026
  return;
3295
5027
  settled = true;
3296
5028
  clearTimeout(timer);
3297
- socket.destroy();
5029
+ options.signal?.removeEventListener("abort", onAbort);
5030
+ destroyAfterCancel();
3298
5031
  run();
3299
5032
  };
3300
- const timer = setTimeout(() => finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten))), timeoutMs);
5033
+ const sendCancel = () => {
5034
+ if (!requestWritten) {
5035
+ cancelAfterWrite = true;
5036
+ return;
5037
+ }
5038
+ if (!socket.writable)
5039
+ return;
5040
+ socket.write(cancelPayload());
5041
+ };
5042
+ const destroyAfterCancel = () => {
5043
+ socket.destroy();
5044
+ };
5045
+ const onAbort = () => {
5046
+ sendCancel();
5047
+ finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
5048
+ };
5049
+ const timer = setTimeout(() => {
5050
+ sendCancel();
5051
+ finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
5052
+ }, options.timeoutMs);
3301
5053
  timer.unref();
5054
+ if (options.signal?.aborted) {
5055
+ onAbort();
5056
+ return;
5057
+ }
5058
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3302
5059
  const decoder = createLineDecoder((message) => {
3303
- const result = toToolResult(message);
5060
+ if (isAuthErrorResponse(message)) {
5061
+ finish(() => reject(new DaemonAuthenticationRejectedError));
5062
+ return;
5063
+ }
5064
+ const result = toToolResult(message, requestId);
3304
5065
  if (result)
3305
- finish(() => resolve6(result));
5066
+ finish(() => resolve9(result));
3306
5067
  else
3307
5068
  finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
3308
5069
  });
3309
5070
  socket.once("connect", () => {
3310
- requestWritten = true;
3311
- socket.write(encodeJsonLine({ jsonrpc: "2.0", id: REQUEST_ID, method: "tools/call", params: { name, arguments: args } }));
5071
+ const payload = encodeJsonLine({
5072
+ jsonrpc: "2.0",
5073
+ id: requestId,
5074
+ method: "tools/call",
5075
+ params: { _omo: authEnvelope(token), name, arguments: args }
5076
+ });
5077
+ socket.write(payload, () => {
5078
+ requestWritten = true;
5079
+ if (cancelAfterWrite && socket.writable)
5080
+ socket.write(cancelPayload());
5081
+ });
3312
5082
  });
3313
5083
  socket.on("data", (chunk) => decoder.push(chunk));
3314
5084
  socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
3315
5085
  socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
3316
5086
  });
3317
5087
  }
3318
- function toToolResult(message) {
3319
- if (!isPlainRecord(message) || message["id"] !== REQUEST_ID)
5088
+ function toToolResult(message, requestId) {
5089
+ if (!isPlainRecord(message) || message["id"] !== requestId)
3320
5090
  return null;
3321
5091
  const result = message["result"];
3322
5092
  if (!isPlainRecord(result) || !Array.isArray(result["content"]))
@@ -3327,167 +5097,436 @@ function toToolResult(message) {
3327
5097
  details: result["details"]
3328
5098
  };
3329
5099
  }
5100
+ function allocateProxyRequestId() {
5101
+ const id = nextProxyRequestId;
5102
+ nextProxyRequestId += 1;
5103
+ if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
5104
+ nextProxyRequestId = 1;
5105
+ return id;
5106
+ }
5107
+ function isRetryableTool(name) {
5108
+ return name !== "rename" && name !== "lsp_rename";
5109
+ }
3330
5110
  function errorText(error) {
3331
5111
  return error instanceof Error ? error.message : String(error);
3332
5112
  }
5113
+ var OMO_LSP_DAEMON_CLI2 = "OMO_LSP_DAEMON_CLI";
5114
+ var OMO_LSP_DAEMON_VERSION2 = "OMO_LSP_DAEMON_VERSION";
5115
+ var DAEMON_VERSION_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
5116
+
5117
+ class InvalidRuntimeOverrideError2 extends Error {
5118
+ code = "invalid_runtime_override";
5119
+ reason;
5120
+ constructor(reason, message) {
5121
+ super(message);
5122
+ this.name = "InvalidRuntimeOverrideError";
5123
+ this.reason = reason;
5124
+ }
5125
+ }
5126
+ function validateDaemonVersion2(version) {
5127
+ if (!DAEMON_VERSION_PATTERN2.test(version)) {
5128
+ throw new Error("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
5129
+ }
5130
+ return version;
5131
+ }
5132
+ function resolveDaemonRuntime2(env, defaults) {
5133
+ const cliOverride = env[OMO_LSP_DAEMON_CLI2];
5134
+ const versionOverride = env[OMO_LSP_DAEMON_VERSION2];
5135
+ const hasCliOverride = cliOverride !== undefined;
5136
+ const hasVersionOverride = versionOverride !== undefined;
5137
+ if (hasCliOverride !== hasVersionOverride) {
5138
+ throw new InvalidRuntimeOverrideError2("paired_values_required", `${OMO_LSP_DAEMON_CLI2} and ${OMO_LSP_DAEMON_VERSION2} must be set together`);
5139
+ }
5140
+ if (!hasCliOverride || !hasVersionOverride) {
5141
+ if (!isAbsolute4(defaults.cliPath)) {
5142
+ throw new InvalidRuntimeOverrideError2("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
5143
+ }
5144
+ return { cliPath: defaults.cliPath, version: validateDaemonVersion2(defaults.version) };
5145
+ }
5146
+ if (!isAbsolute4(cliOverride)) {
5147
+ throw new InvalidRuntimeOverrideError2("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI2} must be an absolute path to an existing regular file`);
5148
+ }
5149
+ const stat = statSync5(cliOverride, { throwIfNoEntry: false });
5150
+ if (!stat) {
5151
+ throw new InvalidRuntimeOverrideError2("cli_not_found", `${OMO_LSP_DAEMON_CLI2} points to a missing file`);
5152
+ }
5153
+ if (!stat.isFile()) {
5154
+ throw new InvalidRuntimeOverrideError2("cli_not_file", `${OMO_LSP_DAEMON_CLI2} must point to a regular file`);
5155
+ }
5156
+ return { cliPath: cliOverride, version: validateDaemonVersion2(versionOverride) };
5157
+ }
5158
+ function callDiagnosticsViaDaemon2(filePath, options) {
5159
+ return callDiagnosticsViaDaemon(filePath, options);
5160
+ }
5161
+
5162
+ // ../../../../lsp-core/src/post-edit/orchestration.ts
5163
+ import { extname as extname2 } from "node:path";
5164
+ var DEFAULT_MAX_CONCURRENCY = 4;
5165
+ var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
5166
+ function createPostEditNotConfiguredCache() {
5167
+ return { notConfiguredExtensions: new Set };
5168
+ }
5169
+ async function collectPostEditDiagnostics(input) {
5170
+ const cache = input.cache === undefined ? createPostEditNotConfiguredCache() : input.cache;
5171
+ const filePaths = firstSeenDiagnosticTargets(input.filePaths, cache);
5172
+ const results = await runBoundedDiagnostics(filePaths, input.runDiagnostics, input.maxConcurrency);
5173
+ const blocks = [];
5174
+ const observations = [];
5175
+ for (const result of results) {
5176
+ const classification = classifyDiagnostics(result.outcome);
5177
+ switch (classification.kind) {
5178
+ case "clean":
5179
+ observations.push({ filePath: result.filePath, kind: "clean" });
5180
+ break;
5181
+ case "not_configured":
5182
+ cache.notConfiguredExtensions.add(classification.extension);
5183
+ observations.push({ filePath: result.filePath, kind: "not_configured" });
5184
+ break;
5185
+ case "block":
5186
+ blocks.push({ filePath: result.filePath, diagnostics: classification.diagnostics });
5187
+ observations.push({ filePath: result.filePath, kind: "block" });
5188
+ break;
5189
+ default: {
5190
+ const exhaustive = classification;
5191
+ return exhaustive;
5192
+ }
5193
+ }
5194
+ }
5195
+ return { blocks, observations };
5196
+ }
5197
+ function firstSeenDiagnosticTargets(filePaths, cache) {
5198
+ const seen = new Set;
5199
+ const targets = [];
5200
+ for (const filePath of filePaths) {
5201
+ if (filePath.length === 0 || seen.has(filePath))
5202
+ continue;
5203
+ seen.add(filePath);
5204
+ const extension = extensionKey(filePath);
5205
+ if (extension !== undefined && cache.notConfiguredExtensions.has(extension))
5206
+ continue;
5207
+ targets.push({ index: targets.length, filePath });
5208
+ }
5209
+ return targets;
5210
+ }
5211
+ async function runBoundedDiagnostics(filePaths, runDiagnostics, maxConcurrency) {
5212
+ const results = [];
5213
+ const workerCount = Math.min(Math.max(1, maxConcurrency ?? DEFAULT_MAX_CONCURRENCY), filePaths.length);
5214
+ let nextIndex = 0;
5215
+ async function worker() {
5216
+ for (;; ) {
5217
+ const target = filePaths[nextIndex];
5218
+ nextIndex += 1;
5219
+ if (target === undefined)
5220
+ return;
5221
+ results[target.index] = {
5222
+ index: target.index,
5223
+ filePath: target.filePath,
5224
+ outcome: await collectFileDiagnostics(target.filePath, runDiagnostics)
5225
+ };
5226
+ }
5227
+ }
5228
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
5229
+ return results.filter((result) => result !== undefined);
5230
+ }
5231
+ async function collectFileDiagnostics(filePath, runDiagnostics) {
5232
+ try {
5233
+ return normalizeDiagnosticsOutcome(await runDiagnostics(filePath));
5234
+ } catch (error) {
5235
+ if (error instanceof Error)
5236
+ return formatDiagnosticsError(error);
5237
+ return normalizeDiagnosticsText(String(error));
5238
+ }
5239
+ }
5240
+ function normalizeDiagnosticsOutcome(outcome) {
5241
+ if (typeof outcome === "string")
5242
+ return normalizeDiagnosticsText(outcome);
5243
+ return outcome;
5244
+ }
5245
+ function normalizeDiagnosticsText(text2) {
5246
+ return text2.trim();
5247
+ }
5248
+ function formatDiagnosticsError(error) {
5249
+ const message = normalizeDiagnosticsText(error.message);
5250
+ return message.length > 0 ? message : normalizeDiagnosticsText(String(error));
5251
+ }
5252
+ function classifyDiagnostics(outcome) {
5253
+ if (typeof outcome !== "string") {
5254
+ return { kind: "not_configured", extension: outcome.extension };
5255
+ }
5256
+ const diagnostics = outcome;
5257
+ if (diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT)
5258
+ return { kind: "clean" };
5259
+ return { kind: "block", diagnostics };
5260
+ }
5261
+ function extensionKey(filePath) {
5262
+ const extension = extname2(filePath).toLowerCase();
5263
+ return extension.length === 0 ? undefined : extension;
5264
+ }
5265
+ // ../../../../lsp-core/src/request-context.ts
5266
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
5267
+ import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync6 } from "node:fs";
5268
+ import { basename as basename3, delimiter as delimiter4, dirname as dirname2, isAbsolute as isAbsolute5, join as join7, relative as relative5, resolve as resolve9 } from "node:path";
3333
5269
 
3334
- // components/lsp/src/codex-hook.ts
3335
- import { readFileSync as readFileSync9 } from "node:fs";
5270
+ class LspRequestContextParseError2 extends Error {
5271
+ code;
5272
+ name = "LspRequestContextParseError";
5273
+ constructor(code, message) {
5274
+ super(message);
5275
+ this.code = code;
5276
+ }
5277
+ }
5278
+ var storage2 = new AsyncLocalStorage2;
5279
+ var CONTEXT_FIELDS2 = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
5280
+ var CAPABILITY_FIELDS2 = new Set(["installDecisionTool"]);
5281
+ function parseLspRequestContext2(value) {
5282
+ if (!isRecord7(value)) {
5283
+ throw new LspRequestContextParseError2("invalid_context", "LSP request context must be an object.");
5284
+ }
5285
+ rejectUnknownFields2(value, CONTEXT_FIELDS2, "context");
5286
+ const cwd = stringField2(value, "cwd");
5287
+ const projectConfigPaths = stringArrayField2(value, "projectConfigPaths");
5288
+ const userConfigPath = stringField2(value, "userConfigPath");
5289
+ const installDecisionsPath = stringField2(value, "installDecisionsPath");
5290
+ const capabilities = capabilitiesField2(value["capabilities"]);
5291
+ const canonical = canonicalCwd2(cwd);
5292
+ for (const path2 of projectConfigPaths) {
5293
+ requireAbsolutePath2(path2, "projectConfigPaths");
5294
+ const projectPath = canonicalizeExistingOrNearestAncestor2(path2);
5295
+ if (!isPathInside2(canonical, projectPath)) {
5296
+ throw new LspRequestContextParseError2("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path2}`);
5297
+ }
5298
+ }
5299
+ requireAbsolutePath2(userConfigPath, "userConfigPath");
5300
+ requireAbsolutePath2(installDecisionsPath, "installDecisionsPath");
5301
+ return {
5302
+ cwd: canonical,
5303
+ projectConfigPaths: projectConfigPaths.map((path2) => canonicalizeExistingOrNearestAncestor2(path2)),
5304
+ userConfigPath,
5305
+ installDecisionsPath,
5306
+ capabilities
5307
+ };
5308
+ }
5309
+ function canonicalCwd2(cwd) {
5310
+ const resolved = resolve9(cwd);
5311
+ if (!existsSync11(resolved) || !statSync6(resolved).isDirectory()) {
5312
+ throw new LspRequestContextParseError2("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
5313
+ }
5314
+ return realpathSync4(resolved);
5315
+ }
5316
+ function canonicalizeExistingOrNearestAncestor2(path2) {
5317
+ let current = resolve9(path2);
5318
+ const suffix = [];
5319
+ while (true) {
5320
+ try {
5321
+ const existing = realpathSync4(current);
5322
+ return suffix.length === 0 ? existing : join7(existing, ...suffix);
5323
+ } catch (error) {
5324
+ if (!isMissingPathError2(error))
5325
+ throw error;
5326
+ const parent = dirname2(current);
5327
+ if (parent === current)
5328
+ throw error;
5329
+ suffix.unshift(basename3(current));
5330
+ current = parent;
5331
+ }
5332
+ }
5333
+ }
5334
+ function capabilitiesField2(value) {
5335
+ if (!isRecord7(value)) {
5336
+ throw new LspRequestContextParseError2("invalid_capabilities", "LSP request capabilities must be an object.");
5337
+ }
5338
+ rejectUnknownFields2(value, CAPABILITY_FIELDS2, "capabilities");
5339
+ const installDecisionTool = value["installDecisionTool"];
5340
+ if (typeof installDecisionTool !== "boolean") {
5341
+ throw new LspRequestContextParseError2("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
5342
+ }
5343
+ return { installDecisionTool };
5344
+ }
5345
+ function stringField2(value, field) {
5346
+ const fieldValue = value[field];
5347
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
5348
+ throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string.`);
5349
+ }
5350
+ return fieldValue;
5351
+ }
5352
+ function stringArrayField2(value, field) {
5353
+ const fieldValue = value[field];
5354
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
5355
+ throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
5356
+ }
5357
+ return fieldValue;
5358
+ }
5359
+ function requireAbsolutePath2(path2, field) {
5360
+ if (!isAbsolute5(path2)) {
5361
+ throw new LspRequestContextParseError2("relative_path", `LSP request context.${field} must be absolute: ${path2}`);
5362
+ }
5363
+ }
5364
+ function isPathInside2(parent, child) {
5365
+ const childPath = resolve9(child);
5366
+ const relativePath = relative5(parent, childPath);
5367
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute5(relativePath);
5368
+ }
5369
+ function isMissingPathError2(error) {
5370
+ const code = errorCode2(error);
5371
+ return code === "ENOENT" || code === "ENOTDIR";
5372
+ }
5373
+ function rejectUnknownFields2(value, allowed, scope) {
5374
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
5375
+ if (unknown.length > 0) {
5376
+ throw new LspRequestContextParseError2("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
5377
+ }
5378
+ }
5379
+ function isRecord7(value) {
5380
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5381
+ }
5382
+ function errorCode2(error) {
5383
+ if (!error || typeof error !== "object" || !("code" in error))
5384
+ return;
5385
+ const code = Reflect.get(error, "code");
5386
+ return typeof code === "string" ? code : undefined;
5387
+ }
3336
5388
 
3337
- // components/lsp/src/daemon-cli-path.ts
3338
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
5389
+ // src/daemon-cli-path.ts
5390
+ import { existsSync as existsSync13, readFileSync as readFileSync6 } from "node:fs";
3339
5391
  import { createRequire as createRequire2 } from "node:module";
3340
- import { dirname as dirname6, join as join9 } from "node:path";
5392
+ import { dirname as dirname9, join as join9 } from "node:path";
3341
5393
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3342
5394
  var requireFromHere2 = createRequire2(import.meta.url);
3343
- var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
3344
- var CODEX_LSP_DAEMON_CLI_ENV2 = "CODEX_LSP_DAEMON_CLI";
3345
- var CODEX_LSP_DAEMON_VERSION_ENV2 = "CODEX_LSP_DAEMON_VERSION";
5395
+ var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/cli";
3346
5396
  function ensureLspDaemonCliEnv(env = process.env) {
3347
- const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV2]?.trim();
3348
- const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
3349
- if (!configuredCli)
3350
- env[CODEX_LSP_DAEMON_CLI_ENV2] = resolution.cliPath;
3351
- if (!env[CODEX_LSP_DAEMON_VERSION_ENV2]?.trim() && resolution.version !== null) {
3352
- env[CODEX_LSP_DAEMON_VERSION_ENV2] = resolution.version;
3353
- }
3354
- }
3355
- function resolveLspDaemonCliPath() {
3356
- return resolveLspDaemonCli().cliPath;
5397
+ const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
5398
+ env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
5399
+ env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
5400
+ }
5401
+ function resolveLspDaemonCliPath(env = process.env) {
5402
+ const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
5403
+ if (env[OMO_LSP_DAEMON_CLI2] === undefined)
5404
+ env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
5405
+ if (env[OMO_LSP_DAEMON_VERSION2] === undefined)
5406
+ env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
5407
+ return runtime.cliPath;
3357
5408
  }
3358
5409
  function resolveLspDaemonCli() {
3359
5410
  const packageCli = resolvePackageLspDaemonCliPath();
3360
5411
  if (packageCli !== null)
3361
5412
  return resolveConfiguredLspDaemonCli(packageCli);
3362
5413
  const bundledCli = fileURLToPath4(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
3363
- if (existsSync9(bundledCli))
5414
+ if (existsSync13(bundledCli))
3364
5415
  return resolveConfiguredLspDaemonCli(bundledCli);
3365
5416
  return resolveConfiguredLspDaemonCli(bundledCli);
3366
5417
  }
3367
5418
  function resolvePackageLspDaemonCliPath() {
3368
5419
  try {
3369
5420
  return requireFromHere2.resolve(PACKAGE_LSP_DAEMON_CLI);
3370
- } catch {
5421
+ } catch (error) {
5422
+ if (!(error instanceof Error))
5423
+ throw error;
3371
5424
  return null;
3372
5425
  }
3373
5426
  }
3374
5427
  function resolveConfiguredLspDaemonCli(cliPath) {
5428
+ const version = readDaemonPackageVersion(cliPath);
5429
+ if (version === null) {
5430
+ throw new Error(`Unable to determine packaged LSP daemon version beside ${cliPath}`);
5431
+ }
3375
5432
  return {
3376
5433
  cliPath,
3377
- version: readDaemonPackageVersion(cliPath)
5434
+ version
3378
5435
  };
3379
5436
  }
3380
5437
  function readDaemonPackageVersion(cliPath) {
3381
5438
  try {
3382
- const parsed = JSON.parse(readFileSync6(join9(dirname6(cliPath), "package.json"), "utf8"));
3383
- if (isRecord4(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
5439
+ const parsed = JSON.parse(readFileSync6(join9(dirname9(cliPath), "package.json"), "utf8"));
5440
+ if (isRecord9(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
3384
5441
  return parsed["version"];
3385
5442
  }
3386
- } catch {}
5443
+ } catch (error) {
5444
+ if (!(error instanceof Error))
5445
+ throw error;
5446
+ }
3387
5447
  return null;
3388
5448
  }
3389
- function isRecord4(value) {
5449
+ function isRecord9(value) {
3390
5450
  return typeof value === "object" && value !== null && !Array.isArray(value);
3391
5451
  }
3392
5452
 
3393
- // components/lsp/src/lsp-session-state.ts
5453
+ // src/lsp-session-state.ts
3394
5454
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
3395
- import { homedir as homedir4 } from "node:os";
3396
- import { dirname as dirname8, extname as extname2, join as join11 } from "node:path";
5455
+ import { homedir } from "node:os";
5456
+ import { dirname as dirname11, join as join10 } from "node:path";
3397
5457
  function sessionIdFrom(input) {
3398
5458
  return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
3399
5459
  }
3400
- function shouldSkipUnavailableLspDiagnostics(filePath, sessionId) {
5460
+ function readLspPostEditCache(sessionId) {
3401
5461
  if (sessionId === undefined)
3402
- return false;
3403
- const state = readSessionState(sessionStatePath(sessionId));
3404
- const extension = extensionKey(filePath);
3405
- return extension !== undefined && state.postCompactProbePending !== true && state.unavailableExtensions.includes(extension);
3406
- }
3407
- function recordLspDiagnosticsObservations(sessionId, observations) {
3408
- if (sessionId === undefined || observations.length === 0)
3409
- return;
5462
+ return { notConfiguredExtensions: new Set };
3410
5463
  const state = readSessionState(sessionStatePath(sessionId));
3411
- const unavailableExtensions = new Set(state.unavailableExtensions);
3412
- for (const observation of observations) {
3413
- const extension = extensionKey(observation.filePath);
3414
- if (extension === undefined)
3415
- continue;
3416
- if (observation.unavailable) {
3417
- unavailableExtensions.add(extension);
3418
- } else {
3419
- unavailableExtensions.delete(extension);
3420
- }
3421
- }
3422
- writeSessionState(sessionStatePath(sessionId), { unavailableExtensions: [...unavailableExtensions].sort() });
5464
+ return { notConfiguredExtensions: new Set(state.notConfiguredExtensions) };
3423
5465
  }
3424
- function markLspSessionCompacted(sessionId) {
5466
+ function writeLspPostEditCache(sessionId, cache) {
3425
5467
  if (sessionId === undefined)
3426
5468
  return;
3427
- const state = readSessionState(sessionStatePath(sessionId));
3428
- if (state.unavailableExtensions.length === 0)
3429
- return;
3430
5469
  writeSessionState(sessionStatePath(sessionId), {
3431
- unavailableExtensions: state.unavailableExtensions,
3432
- postCompactProbePending: true
5470
+ notConfiguredExtensions: [...cache.notConfiguredExtensions].sort()
3433
5471
  });
3434
5472
  }
3435
- function isUnavailableLspDiagnostics(diagnostics) {
3436
- const normalized = diagnostics.trim();
3437
- return normalized.includes("LSP request timeout (method: initialize)") || normalized.includes("LSP server is still initializing") || normalized.includes("NOT INSTALLED") || normalized.includes("Command not found:");
5473
+ function markLspSessionCompacted(sessionId) {
5474
+ if (sessionId === undefined)
5475
+ return;
5476
+ writeSessionState(sessionStatePath(sessionId), emptyState());
3438
5477
  }
3439
5478
  function isLspDaemonUnreachableDiagnostics(diagnostics) {
3440
5479
  return diagnostics.includes("LSP daemon unreachable");
3441
5480
  }
3442
5481
  function sessionStatePath(sessionId) {
3443
- const root = process.env["PLUGIN_DATA"] ?? join11(homedir4(), ".codex", "codex-lsp");
3444
- return join11(root, "sessions", `${safePathSegment(sessionId)}.json`);
5482
+ const root = process.env["PLUGIN_DATA"] ?? join10(homedir(), ".codex", "codex-lsp");
5483
+ return join10(root, "sessions", `${safePathSegment(sessionId)}.json`);
3445
5484
  }
3446
- function readSessionState(path) {
5485
+ function readSessionState(path2) {
3447
5486
  try {
3448
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
3449
- if (isLspSessionState(parsed))
3450
- return parsed;
5487
+ const parsed = JSON.parse(readFileSync8(path2, "utf8"));
5488
+ if (isRecord10(parsed) && isLspSessionState(parsed))
5489
+ return normalizeSessionState(parsed);
3451
5490
  return emptyState();
3452
5491
  } catch (error) {
3453
- if (error instanceof SyntaxError || isRecord6(error) && error["code"] === "ENOENT")
5492
+ if (error instanceof SyntaxError || isRecord10(error) && error["code"] === "ENOENT")
3454
5493
  return emptyState();
3455
5494
  throw error;
3456
5495
  }
3457
5496
  }
3458
- function writeSessionState(path, state) {
3459
- mkdirSync4(dirname8(path), { recursive: true });
3460
- writeFileSync3(path, `${JSON.stringify(state)}
5497
+ function writeSessionState(path2, state) {
5498
+ mkdirSync4(dirname11(path2), { recursive: true });
5499
+ writeFileSync3(path2, `${JSON.stringify(state)}
3461
5500
  `);
3462
5501
  }
3463
5502
  function emptyState() {
3464
- return { unavailableExtensions: [] };
3465
- }
3466
- function extensionKey(filePath) {
3467
- const extension = extname2(filePath).toLowerCase();
3468
- return extension.length === 0 ? undefined : extension;
5503
+ return { notConfiguredExtensions: [] };
3469
5504
  }
3470
5505
  function safePathSegment(value) {
3471
5506
  return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
3472
5507
  }
3473
5508
  function isLspSessionState(value) {
3474
- if (!isRecord6(value) || !Array.isArray(value["unavailableExtensions"]))
3475
- return false;
3476
- const postCompactProbePending = value["postCompactProbePending"];
3477
- return value["unavailableExtensions"].every((item) => typeof item === "string") && (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean");
5509
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
5510
+ return Array.isArray(notConfiguredExtensions) && notConfiguredExtensions.every((item) => typeof item === "string");
3478
5511
  }
3479
- function isRecord6(value) {
5512
+ function normalizeSessionState(value) {
5513
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
5514
+ return {
5515
+ notConfiguredExtensions: Array.isArray(notConfiguredExtensions) ? notConfiguredExtensions.filter((item) => typeof item === "string").sort() : []
5516
+ };
5517
+ }
5518
+ function isRecord10(value) {
3480
5519
  return typeof value === "object" && value !== null && !Array.isArray(value);
3481
5520
  }
3482
5521
 
3483
- // components/lsp/src/mutated-file-paths.ts
5522
+ // src/mutated-file-paths.ts
3484
5523
  var MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
3485
5524
  function extractMutatedFilePaths(input) {
3486
5525
  if (!isMutationTool(input.tool_name))
3487
5526
  return [];
3488
5527
  if (isFailedToolResponse(input.tool_response))
3489
5528
  return [];
3490
- const toolInput = isRecord7(input.tool_input) ? input.tool_input : {};
5529
+ const toolInput = isRecord11(input.tool_input) ? input.tool_input : {};
3491
5530
  const paths = new Set;
3492
5531
  addStringValue(paths, toolInput["path"]);
3493
5532
  addStringValue(paths, toolInput["filePath"]);
@@ -3506,7 +5545,7 @@ function isMutationTool(value) {
3506
5545
  return MUTATION_TOOL_NAMES.has(value.toLowerCase());
3507
5546
  }
3508
5547
  function isFailedToolResponse(value) {
3509
- if (!isRecord7(value))
5548
+ if (!isRecord11(value))
3510
5549
  return false;
3511
5550
  return value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error";
3512
5551
  }
@@ -3532,9 +5571,9 @@ function addPatchInput(paths, value) {
3532
5571
  return;
3533
5572
  for (const line of value.split(`
3534
5573
  `)) {
3535
- const path = extractPatchHeaderPath(line);
3536
- if (path !== undefined)
3537
- paths.add(path);
5574
+ const path2 = extractPatchHeaderPath(line);
5575
+ if (path2 !== undefined)
5576
+ paths.add(path2);
3538
5577
  }
3539
5578
  }
3540
5579
  function extractPatchHeaderPath(line) {
@@ -3549,7 +5588,7 @@ function addPatchFiles(paths, value) {
3549
5588
  if (!Array.isArray(value))
3550
5589
  return;
3551
5590
  for (const item of value) {
3552
- if (!isRecord7(item))
5591
+ if (!isRecord11(item))
3553
5592
  continue;
3554
5593
  addStringValue(paths, item["path"]);
3555
5594
  addStringValue(paths, item["filePath"]);
@@ -3558,18 +5597,15 @@ function addPatchFiles(paths, value) {
3558
5597
  addStringValue(paths, item["move_path"]);
3559
5598
  }
3560
5599
  }
3561
- function isRecord7(value) {
5600
+ function isRecord11(value) {
3562
5601
  return typeof value === "object" && value !== null && !Array.isArray(value);
3563
5602
  }
3564
5603
 
3565
- // components/lsp/src/codex-hook.ts
3566
- var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
3567
- var UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
5604
+ // src/codex-hook.ts
3568
5605
  var DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
3569
5606
  var DIAGNOSTIC_CHUNK_PATTERN = /^(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/;
3570
5607
  var DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
3571
5608
  var CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
3572
- var MAX_CONCURRENT_DIAGNOSTICS = 4;
3573
5609
  var CONTEXT_PRESSURE_MARKERS = [
3574
5610
  "context compacted",
3575
5611
  "context_length_exceeded",
@@ -3581,29 +5617,47 @@ var CONTEXT_PRESSURE_MARKERS = [
3581
5617
  ];
3582
5618
  async function runLspDiagnosticsText(filePath) {
3583
5619
  ensureLspDaemonCliEnv();
3584
- const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
5620
+ const result = await callDiagnosticsViaDaemon2(filePath, { context: codexLspRequestContext() });
5621
+ return postEditOutcomeFromDaemonResult(result);
5622
+ }
5623
+ function postEditOutcomeFromDaemonResult(result) {
5624
+ const availability = notConfiguredAvailability(result.details);
5625
+ if (availability !== undefined)
5626
+ return { kind: "not_configured", extension: availability.extension };
3585
5627
  return result.content.map((block) => block.text).join(`
3586
5628
  `);
3587
5629
  }
5630
+ function notConfiguredAvailability(details) {
5631
+ if (!isRecord12(details))
5632
+ return;
5633
+ const availability = details["availability"];
5634
+ if (!isRecord12(availability))
5635
+ return;
5636
+ if (availability["kind"] !== "not_configured")
5637
+ return;
5638
+ const extension = availability["extension"];
5639
+ return typeof extension === "string" && extension.length > 0 ? { extension } : undefined;
5640
+ }
5641
+ function codexLspRequestContext(env = process.env, cwd = process.cwd()) {
5642
+ const canonicalCwd3 = realpathSync6(resolve11(cwd));
5643
+ const codexHome = resolve11(env["CODEX_HOME"]?.trim() || join11(homedir3(), ".codex"));
5644
+ return parseLspRequestContext2({
5645
+ cwd: canonicalCwd3,
5646
+ projectConfigPaths: [join11(canonicalCwd3, ".codex", "lsp-client.json")],
5647
+ userConfigPath: join11(codexHome, "lsp-client.json"),
5648
+ installDecisionsPath: join11(codexHome, "lsp-install-decisions.json"),
5649
+ capabilities: { installDecisionTool: true }
5650
+ });
5651
+ }
3588
5652
  async function runLspPostToolUseHook(input, runDiagnostics = runLspDiagnosticsText) {
3589
5653
  const sessionId = sessionIdFrom(input);
3590
- const filePaths = extractMutatedFilePaths(input).filter((filePath) => !shouldSkipUnavailableLspDiagnostics(filePath, sessionId));
5654
+ const filePaths = extractMutatedFilePaths(input);
3591
5655
  if (filePaths.length === 0)
3592
5656
  return "";
3593
- const blocks = [];
3594
- const observations = [];
3595
- for (const { filePath, diagnostics } of await collectDiagnostics(filePaths, runDiagnostics)) {
3596
- if (isLspDaemonUnreachableDiagnostics(diagnostics))
3597
- continue;
3598
- const unavailable = isUnavailableLspDiagnostics(diagnostics);
3599
- observations.push({ filePath, unavailable });
3600
- if (isCleanDiagnostics(diagnostics))
3601
- continue;
3602
- if (unavailable)
3603
- continue;
3604
- blocks.push({ filePath, diagnostics });
3605
- }
3606
- recordLspDiagnosticsObservations(sessionId, observations);
5657
+ const cache = readLspPostEditCache(sessionId);
5658
+ const result = await collectPostEditDiagnostics({ filePaths, runDiagnostics, cache });
5659
+ writeLspPostEditCache(sessionId, cache);
5660
+ const blocks = result.blocks.filter(({ diagnostics }) => !isLspDaemonUnreachableDiagnostics(diagnostics));
3607
5661
  if (blocks.length === 0)
3608
5662
  return "";
3609
5663
  const rawReason = blocks.map(formatDiagnosticBlock).join(`
@@ -3625,38 +5679,6 @@ async function runLspPostCompactHook(input) {
3625
5679
  markLspSessionCompacted(sessionIdFrom(input));
3626
5680
  return "";
3627
5681
  }
3628
- async function collectDiagnostics(filePaths, runDiagnostics) {
3629
- const results = [];
3630
- let nextIndex = 0;
3631
- const workerCount = Math.min(MAX_CONCURRENT_DIAGNOSTICS, filePaths.length);
3632
- async function worker() {
3633
- for (;; ) {
3634
- const index = nextIndex;
3635
- nextIndex += 1;
3636
- const filePath = filePaths[index];
3637
- if (filePath === undefined)
3638
- return;
3639
- results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
3640
- }
3641
- }
3642
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
3643
- return results;
3644
- }
3645
- async function collectFileDiagnostics(filePath, runDiagnostics) {
3646
- try {
3647
- return (await runDiagnostics(filePath)).trim();
3648
- } catch (error) {
3649
- return formatDiagnosticsError(error);
3650
- }
3651
- }
3652
- function formatDiagnosticsError(error) {
3653
- if (error instanceof Error) {
3654
- const message = error.message.trim();
3655
- if (message.length > 0)
3656
- return message;
3657
- }
3658
- return String(error).trim();
3659
- }
3660
5682
  function formatDiagnosticBlock({ filePath, diagnostics }) {
3661
5683
  return `LSP diagnostics after editing ${filePath}:
3662
5684
 
@@ -3739,14 +5761,11 @@ function limitHookText(text2, maxChars) {
3739
5761
  const head = text2.slice(0, maxChars - marker.length).replace(/[ \t\r\n]+$/, "");
3740
5762
  return `${head}${marker}`;
3741
5763
  }
3742
- function isCleanDiagnostics(diagnostics) {
3743
- return diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT || diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT);
3744
- }
3745
- function isRecord8(value) {
5764
+ function isRecord12(value) {
3746
5765
  return typeof value === "object" && value !== null && !Array.isArray(value);
3747
5766
  }
3748
5767
 
3749
- // components/lsp/src/codex-hook-cli.ts
5768
+ // src/codex-hook-cli.ts
3750
5769
  async function runPostToolUseHookCli(stdin = processStdin) {
3751
5770
  await runHookCli((input) => runLspPostToolUseHook(input), stdin);
3752
5771
  }
@@ -3759,16 +5778,12 @@ async function runHookCli(runHook, stdin) {
3759
5778
  if (!raw.trim())
3760
5779
  return;
3761
5780
  const parsed = JSON.parse(raw);
3762
- const input = isRecord8(parsed) ? parsed : {};
5781
+ const input = isRecord12(parsed) ? parsed : {};
3763
5782
  const output = await runHook(input);
3764
5783
  if (output)
3765
5784
  process.stdout.write(output);
3766
5785
  } catch {
3767
5786
  return;
3768
- } finally {
3769
- await disposeDefaultLspManager().catch(() => {
3770
- return;
3771
- });
3772
5787
  }
3773
5788
  }
3774
5789
  async function readStdin(stdin) {
@@ -3780,7 +5795,7 @@ async function readStdin(stdin) {
3780
5795
  return raw;
3781
5796
  }
3782
5797
 
3783
- // components/lsp/src/cli.ts
5798
+ // src/cli.ts
3784
5799
  async function main() {
3785
5800
  const [command = "mcp", subcommand = ""] = argv.slice(2);
3786
5801
  if (command === "hook" && subcommand === "post-tool-use") {
@@ -3807,14 +5822,14 @@ main().catch((error) => {
3807
5822
  async function runPackageLspMcpCli() {
3808
5823
  const cliPath = resolveLspDaemonCliPath();
3809
5824
  const child = spawn3(execPath2, [cliPath, "mcp"], { stdio: "inherit" });
3810
- await new Promise((resolve6, reject) => {
5825
+ await new Promise((resolve10, reject) => {
3811
5826
  child.once("error", reject);
3812
5827
  child.once("exit", (code, signal) => {
3813
5828
  if (code !== null && code !== 0)
3814
5829
  process.exitCode = code;
3815
5830
  if (code === null && signal !== null)
3816
5831
  process.exitCode = 1;
3817
- resolve6();
5832
+ resolve10();
3818
5833
  });
3819
5834
  });
3820
5835
  }