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
@@ -0,0 +1,215 @@
1
+ import { appendFileSync, readFileSync } from "node:fs";
2
+
3
+ const [, , scenarioPath, eventsPath] = process.argv;
4
+ const scenario = JSON.parse(readFileSync(scenarioPath, "utf-8"));
5
+
6
+ let buffer = Buffer.alloc(0);
7
+ let nextServerRequestId = 10_000;
8
+ let renameIndex = 0;
9
+ let diagnosticIndex = 0;
10
+ const pendingServerRequests = new Map();
11
+
12
+ function record(event) {
13
+ appendFileSync(eventsPath, `${JSON.stringify(event)}\n`, "utf-8");
14
+ }
15
+
16
+ function send(message) {
17
+ const body = Buffer.from(JSON.stringify(message), "utf-8");
18
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
19
+ process.stdout.write(body);
20
+ }
21
+
22
+ function sendResponse(id, result) {
23
+ send({ jsonrpc: "2.0", id, result });
24
+ }
25
+
26
+ function sendError(id, code, message) {
27
+ send({ jsonrpc: "2.0", id, error: { code, message } });
28
+ }
29
+
30
+ function sendApplyEdit(edit, context) {
31
+ const id = nextServerRequestId;
32
+ nextServerRequestId += 1;
33
+ pendingServerRequests.set(String(id), context);
34
+ record({ type: "serverRequest", method: "workspace/applyEdit", id, edit });
35
+ send({ jsonrpc: "2.0", id, method: "workspace/applyEdit", params: { edit } });
36
+ }
37
+
38
+ function renameResult(step) {
39
+ if (step.renameResult === "same") return step.applyEdit ?? null;
40
+ if (step.renameResult === "different") return step.differentEdit ?? null;
41
+ if (step.renameResult === "edit") return step.renameEdit ?? null;
42
+ return null;
43
+ }
44
+
45
+ function finishRename(context) {
46
+ const result = renameResult(context.step);
47
+ const delay = Number(context.step.responseDelayMs ?? 0);
48
+ setTimeout(() => {
49
+ record({ type: "serverResponse", method: "textDocument/rename", result });
50
+ sendResponse(context.renameRequestId, result);
51
+ }, delay);
52
+ }
53
+
54
+ function schedulePublish(step, fallbackUri, fallbackVersion) {
55
+ const delay = Number(step.delayMs ?? 0);
56
+ setTimeout(() => {
57
+ const params = {
58
+ uri: step.uri ?? fallbackUri,
59
+ ...(step.version === undefined ? {} : { version: step.version }),
60
+ diagnostics: Array.isArray(step.diagnostics) ? step.diagnostics : [],
61
+ };
62
+ record({ type: "serverNotification", method: "textDocument/publishDiagnostics", params });
63
+ send({ jsonrpc: "2.0", method: "textDocument/publishDiagnostics", params });
64
+ }, delay);
65
+ }
66
+
67
+ function publishSteps(trigger) {
68
+ if (!Array.isArray(scenario.publishDiagnostics)) return [];
69
+ return scenario.publishDiagnostics.filter((step) => step?.trigger === trigger);
70
+ }
71
+
72
+ function applyPublishSteps(trigger, fallbackUri, fallbackVersion) {
73
+ for (const step of publishSteps(trigger)) schedulePublish(step, fallbackUri, fallbackVersion);
74
+ }
75
+
76
+ function handleClientResponse(message) {
77
+ const context = pendingServerRequests.get(String(message.id));
78
+ if (!context) return;
79
+ pendingServerRequests.delete(String(message.id));
80
+ record({
81
+ type: "clientResponse",
82
+ method: "workspace/applyEdit",
83
+ result: message.result ?? null,
84
+ error: message.error ?? null,
85
+ });
86
+ if (context.kind === "unscoped") return;
87
+ if (typeof context.pendingApplyResponses === "number") {
88
+ context.pendingApplyResponses -= 1;
89
+ if (context.pendingApplyResponses > 0) return;
90
+ finishRename(context);
91
+ return;
92
+ }
93
+ if (context.step.applyTwice === true && context.applyCount === 1) {
94
+ sendApplyEdit(context.step.applyEdit, { ...context, applyCount: 2 });
95
+ return;
96
+ }
97
+ finishRename(context);
98
+ }
99
+
100
+ function handleRequest(message) {
101
+ if (message.method === "initialize") {
102
+ record({ type: "clientRequest", method: message.method, id: message.id, params: message.params });
103
+ sendResponse(message.id, { capabilities: scenario.capabilities ?? {} });
104
+ return;
105
+ }
106
+ if (message.method === "textDocument/rename") {
107
+ const step = scenario.renameSteps?.[renameIndex] ?? {};
108
+ renameIndex += 1;
109
+ record({ type: "clientRequest", method: message.method, id: message.id, params: message.params, renameIndex });
110
+ if (step.applyEdit) {
111
+ const context = { kind: "rename", renameRequestId: message.id, step, applyCount: 1 };
112
+ if (step.applyConcurrently === true) {
113
+ context.pendingApplyResponses = 2;
114
+ sendApplyEdit(step.applyEdit, context);
115
+ sendApplyEdit(step.applyEdit, context);
116
+ return;
117
+ }
118
+ const delay = Number(step.applyDelayMs ?? 0);
119
+ if (delay > 0) {
120
+ setTimeout(() => sendApplyEdit(step.applyEdit, context), delay);
121
+ return;
122
+ }
123
+ sendApplyEdit(step.applyEdit, context);
124
+ return;
125
+ }
126
+ finishRename({ renameRequestId: message.id, step });
127
+ return;
128
+ }
129
+ if (message.method === "textDocument/diagnostic") {
130
+ record({ type: "clientRequest", method: message.method, id: message.id, params: message.params, diagnosticIndex: diagnosticIndex + 1 });
131
+ applyPublishSteps("diagnosticRequest", message.params?.textDocument?.uri, undefined);
132
+ const step = Array.isArray(scenario.diagnosticResponses) ? scenario.diagnosticResponses[diagnosticIndex] : undefined;
133
+ diagnosticIndex += 1;
134
+ if (step?.action === "hang") return;
135
+ if (step?.action === "close") {
136
+ const delay = Number(step.delayMs ?? 0);
137
+ setTimeout(() => process.exit(0), delay);
138
+ return;
139
+ }
140
+ const delay = Number(step?.delayMs ?? 0);
141
+ setTimeout(() => {
142
+ if (step?.error) {
143
+ record({ type: "serverError", method: "textDocument/diagnostic", error: step.error });
144
+ sendError(message.id, step.error.code, step.error.message);
145
+ return;
146
+ }
147
+ const result = step?.report ?? { items: scenario.diagnostics ?? [] };
148
+ record({ type: "serverResponse", method: "textDocument/diagnostic", result });
149
+ sendResponse(message.id, result);
150
+ }, delay);
151
+ return;
152
+ }
153
+ if (message.method === "shutdown") {
154
+ sendResponse(message.id, null);
155
+ return;
156
+ }
157
+ sendResponse(message.id, null);
158
+ }
159
+
160
+ function handleNotification(message) {
161
+ record({ type: "clientNotification", method: message.method, params: message.params ?? null });
162
+ if (message.method === "initialized" && scenario.unscopedApplyEdit) {
163
+ sendApplyEdit(scenario.unscopedApplyEdit, { kind: "unscoped" });
164
+ }
165
+ if (message.method === "initialized") {
166
+ applyPublishSteps("initialized", undefined, undefined);
167
+ }
168
+ if (message.method === "textDocument/didOpen") {
169
+ applyPublishSteps("didOpen", message.params.textDocument.uri, message.params.textDocument.version);
170
+ }
171
+ if (message.method === "textDocument/didChange" && Array.isArray(scenario.diagnostics)) {
172
+ send({
173
+ jsonrpc: "2.0",
174
+ method: "textDocument/publishDiagnostics",
175
+ params: {
176
+ uri: message.params.textDocument.uri,
177
+ version: message.params.textDocument.version,
178
+ diagnostics: scenario.diagnostics,
179
+ },
180
+ });
181
+ }
182
+ if (message.method === "textDocument/didChange") {
183
+ applyPublishSteps("didChange", message.params.textDocument.uri, message.params.textDocument.version);
184
+ }
185
+ if (message.method === "exit") process.exit(0);
186
+ }
187
+
188
+ function handleMessage(message) {
189
+ if (Object.hasOwn(message, "id") && (Object.hasOwn(message, "result") || Object.hasOwn(message, "error"))) {
190
+ handleClientResponse(message);
191
+ return;
192
+ }
193
+ if (Object.hasOwn(message, "id")) {
194
+ handleRequest(message);
195
+ return;
196
+ }
197
+ handleNotification(message);
198
+ }
199
+
200
+ process.stdin.on("data", (chunk) => {
201
+ buffer = Buffer.concat([buffer, chunk]);
202
+ for (;;) {
203
+ const headerEnd = buffer.indexOf("\r\n\r\n");
204
+ if (headerEnd === -1) return;
205
+ const header = buffer.subarray(0, headerEnd).toString("ascii");
206
+ const match = /content-length:\s*(\d+)/i.exec(header);
207
+ if (!match) process.exit(2);
208
+ const length = Number(match[1]);
209
+ const bodyStart = headerEnd + 4;
210
+ if (buffer.length < bodyStart + length) return;
211
+ const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf-8");
212
+ buffer = buffer.subarray(bodyStart + length);
213
+ handleMessage(JSON.parse(body));
214
+ }
215
+ });
@@ -127,6 +127,9 @@ export function formatApplyResult(result: ApplyResult): string {
127
127
  for (const file of result.filesModified) {
128
128
  lines.push(` - ${file}`);
129
129
  }
130
+ if (result.lateAbort) {
131
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
132
+ }
130
133
  } else {
131
134
  lines.push("Failed to apply some changes:");
132
135
  for (const err of result.errors) {
@@ -0,0 +1,97 @@
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+
5
+ import { JsonRpcConnection } from "./json-rpc-connection.js";
6
+
7
+ type JsonMessage = Record<string, unknown>;
8
+
9
+ function messageId(message: JsonMessage): string | number | null | undefined {
10
+ const id = message["id"];
11
+ return typeof id === "string" || typeof id === "number" || id === null ? id : undefined;
12
+ }
13
+
14
+ function cancelTarget(message: JsonMessage): string | number | undefined {
15
+ const params = message["params"];
16
+ if (!params || typeof params !== "object" || Array.isArray(params)) return undefined;
17
+ const id = Reflect.get(params, "id");
18
+ return typeof id === "string" || typeof id === "number" ? id : undefined;
19
+ }
20
+
21
+ function encodeMessage(message: JsonMessage): string {
22
+ const body = JSON.stringify(message);
23
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
24
+ }
25
+
26
+ function recordJsonRpcMessages(stream: PassThrough) {
27
+ const messages: JsonMessage[] = [];
28
+ const waiters: Array<() => void> = [];
29
+ let buffer = Buffer.alloc(0);
30
+ stream.on("data", (chunk: Buffer) => {
31
+ buffer = Buffer.concat([buffer, chunk]);
32
+ for (;;) {
33
+ const headerEnd = buffer.indexOf("\r\n\r\n");
34
+ if (headerEnd === -1) return;
35
+ const header = buffer.subarray(0, headerEnd).toString("ascii");
36
+ const match = /content-length:\s*(\d+)/i.exec(header);
37
+ if (!match?.[1]) throw new Error("missing content-length");
38
+ const bodyStart = headerEnd + 4;
39
+ const bodyEnd = bodyStart + Number.parseInt(match[1], 10);
40
+ if (buffer.length < bodyEnd) return;
41
+ const parsed: unknown = JSON.parse(buffer.subarray(bodyStart, bodyEnd).toString("utf8"));
42
+ buffer = buffer.subarray(bodyEnd);
43
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
44
+ messages.push(Object.fromEntries(Object.entries(parsed)));
45
+ }
46
+ for (const wake of waiters.splice(0)) wake();
47
+ }
48
+ });
49
+ return {
50
+ messages,
51
+ waitForCount(count: number): Promise<void> {
52
+ if (messages.length >= count) return Promise.resolve();
53
+ return new Promise((resolve, reject) => {
54
+ const timer = setTimeout(() => {
55
+ reject(new Error(`waited for ${count} JSON-RPC messages, observed ${messages.length}`));
56
+ }, 1_000);
57
+ const wake = (): void => {
58
+ if (messages.length < count) {
59
+ waiters.push(wake);
60
+ return;
61
+ }
62
+ clearTimeout(timer);
63
+ resolve();
64
+ };
65
+ waiters.push(wake);
66
+ });
67
+ },
68
+ };
69
+ }
70
+
71
+ describe("JsonRpcConnection cancellation", () => {
72
+ it("#given an active request #when its signal aborts #then cancel uses the same id and late responses are ignored", async () => {
73
+ const serverToClient = new PassThrough();
74
+ const clientToServer = new PassThrough();
75
+ const recorder = recordJsonRpcMessages(clientToServer);
76
+ const connection = new JsonRpcConnection(serverToClient, clientToServer);
77
+ connection.listen();
78
+ const controller = new AbortController();
79
+
80
+ const request = connection.sendRequest("textDocument/diagnostic", { textDocument: { uri: "file:///a.ts" } }, {
81
+ signal: controller.signal,
82
+ });
83
+ await recorder.waitForCount(1);
84
+ const requestId = messageId(recorder.messages[0] ?? {});
85
+ expect(typeof requestId === "string" || typeof requestId === "number").toBe(true);
86
+
87
+ controller.abort(new Error("caller cancelled"));
88
+ await recorder.waitForCount(2);
89
+ await expect(request).rejects.toThrow("caller cancelled");
90
+ expect(connection.pendingRequestCount()).toBe(0);
91
+ expect(recorder.messages[1]?.["method"]).toBe("$/cancelRequest");
92
+ expect(cancelTarget(recorder.messages[1] ?? {})).toBe(typeof requestId === "number" || typeof requestId === "string" ? requestId : undefined);
93
+
94
+ serverToClient.write(encodeMessage({ jsonrpc: "2.0", id: requestId ?? null, result: { items: [] } }));
95
+ expect(connection.pendingRequestCount()).toBe(0);
96
+ });
97
+ });
@@ -3,11 +3,16 @@ type JsonRpcId = number | string | null;
3
3
  interface PendingRequest {
4
4
  resolve(result: unknown): void;
5
5
  reject(error: Error): void;
6
+ cleanup(): void;
6
7
  }
7
8
 
8
9
  type NotificationHandler = (params: unknown) => void;
9
10
  type RequestHandler = (params: unknown) => Promise<unknown> | unknown;
10
11
 
12
+ export interface JsonRpcRequestOptions {
13
+ readonly signal?: AbortSignal;
14
+ }
15
+
11
16
  const HEADER_SEPARATOR = "\r\n\r\n";
12
17
  const PARSE_ERROR = -32700;
13
18
  const INVALID_REQUEST = -32600;
@@ -56,32 +61,81 @@ export class JsonRpcConnection {
56
61
  this.errorHandlers.push(handler);
57
62
  }
58
63
 
59
- async sendRequest<T>(method: string, params?: unknown): Promise<T> {
64
+ async sendRequest<T>(method: string, params?: unknown, options: JsonRpcRequestOptions = {}): Promise<T> {
60
65
  if (this.disposed) throw new Error("JSON-RPC connection is disposed");
61
66
 
62
67
  const id = this.nextRequestId;
63
68
  this.nextRequestId += 1;
69
+ const key = String(id);
64
70
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
71
+ let requestWritten = false;
72
+ let cancelAfterWrite = false;
73
+ let settled = false;
74
+ const writeCancel = (): Promise<void> => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
65
75
 
66
76
  const responsePromise = new Promise<T>((resolve, reject) => {
67
- this.pendingRequests.set(String(id), {
77
+ const cleanup = (): void => {
78
+ options.signal?.removeEventListener("abort", onAbort);
79
+ };
80
+ const settleCancel = (): void => {
81
+ if (settled) return;
82
+ settled = true;
83
+ this.pendingRequests.delete(key);
84
+ cleanup();
85
+ const rejectCancelled = (): void => reject(abortError(options.signal));
86
+ if (!requestWritten) {
87
+ cancelAfterWrite = true;
88
+ rejectCancelled();
89
+ return;
90
+ }
91
+ void writeCancel().then(rejectCancelled, (error) => {
92
+ this.emitError(toError(error));
93
+ rejectCancelled();
94
+ });
95
+ };
96
+ const onAbort = (): void => settleCancel();
97
+ this.pendingRequests.set(key, {
68
98
  resolve(result) {
99
+ settled = true;
100
+ cleanup();
69
101
  resolve(result as T);
70
102
  },
71
- reject,
103
+ reject(error) {
104
+ settled = true;
105
+ cleanup();
106
+ reject(error);
107
+ },
108
+ cleanup,
72
109
  });
110
+ if (options.signal?.aborted) {
111
+ settleCancel();
112
+ return;
113
+ }
114
+ options.signal?.addEventListener("abort", onAbort, { once: true });
73
115
  });
74
116
 
117
+ if (settled) return responsePromise;
75
118
  try {
76
119
  await this.writeMessage(message);
120
+ requestWritten = true;
121
+ if (cancelAfterWrite) await writeCancel();
77
122
  } catch (error) {
78
- this.pendingRequests.delete(String(id));
123
+ if (settled) return responsePromise;
124
+ const pending = this.pendingRequests.get(key);
125
+ if (pending) {
126
+ pending.cleanup();
127
+ this.pendingRequests.delete(key);
128
+ }
79
129
  throw error;
80
130
  }
81
131
 
82
132
  return responsePromise;
83
133
  }
84
134
 
135
+ pendingRequestCount(): number {
136
+ return this.pendingRequests.size;
137
+ }
138
+
85
139
  async sendNotification(method: string, params?: unknown): Promise<void> {
86
140
  if (this.disposed) return;
87
141
  const message = params === undefined ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params };
@@ -97,6 +151,7 @@ export class JsonRpcConnection {
97
151
  this.reader.off("error", this.handleStreamError);
98
152
  this.writer.off("error", this.handleStreamError);
99
153
  for (const pending of this.pendingRequests.values()) {
154
+ pending.cleanup();
100
155
  pending.reject(new Error("JSON-RPC connection disposed"));
101
156
  }
102
157
  this.pendingRequests.clear();
@@ -188,6 +243,7 @@ export class JsonRpcConnection {
188
243
  const pending = this.pendingRequests.get(String(id));
189
244
  if (!pending) return;
190
245
  this.pendingRequests.delete(String(id));
246
+ pending.cleanup();
191
247
 
192
248
  if ("error" in message) {
193
249
  pending.reject(jsonRpcErrorToError(message["error"]));
@@ -203,7 +259,11 @@ export class JsonRpcConnection {
203
259
  try {
204
260
  handler(params);
205
261
  } catch (error) {
206
- this.emitError(toError(error));
262
+ if (error instanceof Error) {
263
+ this.emitError(error);
264
+ return;
265
+ }
266
+ this.emitError(new Error(String(error)));
207
267
  }
208
268
  }
209
269
 
@@ -259,6 +319,14 @@ export class JsonRpcConnection {
259
319
  }
260
320
  }
261
321
 
322
+ function abortError(signal: AbortSignal | undefined): Error {
323
+ const reason = signal?.reason;
324
+ if (reason instanceof Error) return reason;
325
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
326
+ error.name = "AbortError";
327
+ return error;
328
+ }
329
+
262
330
  function parseContentLength(headers: string): number | null {
263
331
  for (const line of headers.split("\r\n")) {
264
332
  const separatorIndex = line.indexOf(":");
@@ -1,8 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join } from "node:path";
2
+ import { dirname } from "node:path";
4
3
 
5
- import { contextEnv } from "../request-context.js";
4
+ import { lspRequestContext } from "../request-context.js";
6
5
 
7
6
  export type InstallDecision = "declined" | "allowed";
8
7
 
@@ -14,9 +13,7 @@ export interface InstallDecisionRecord {
14
13
  type InstallDecisions = Record<string, InstallDecisionRecord>;
15
14
 
16
15
  export function getInstallDecisionsPath(): string {
17
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
18
- if (!override) return join(homedir(), ".codex", "lsp-install-decisions.json");
19
- return isAbsolute(override) ? override : join(homedir(), override);
16
+ return lspRequestContext().installDecisionsPath;
20
17
  }
21
18
 
22
19
  export function loadInstallDecisions(): InstallDecisions {
@@ -0,0 +1,52 @@
1
+ import type { Diagnostic } from "./types.js";
2
+
3
+ interface ConfigurationItem {
4
+ readonly section?: string;
5
+ }
6
+
7
+ interface DiagnosticsParams {
8
+ readonly uri: string;
9
+ readonly diagnostics: Diagnostic[];
10
+ readonly version?: number;
11
+ }
12
+
13
+ function isRecord(value: unknown): value is Record<string, unknown> {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+
17
+ export function parseConfigurationItems(params: unknown): ConfigurationItem[] {
18
+ if (!isRecord(params) || !Array.isArray(params["items"])) return [];
19
+ const items: ConfigurationItem[] = [];
20
+ for (const item of params["items"]) {
21
+ if (!isRecord(item)) continue;
22
+ const section = item["section"];
23
+ items.push(section === undefined || typeof section !== "string" ? {} : { section });
24
+ }
25
+ return items;
26
+ }
27
+
28
+ export function parseDiagnosticsParams(params: unknown): DiagnosticsParams | null {
29
+ if (!isRecord(params) || typeof params["uri"] !== "string") return null;
30
+ const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
31
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
32
+ return { uri: params["uri"], diagnostics, ...(version === undefined ? {} : { version }) };
33
+ }
34
+
35
+ export function createLspSpawnEnv(
36
+ _root: string,
37
+ input: Record<string, string | undefined>,
38
+ ): Record<string, string | undefined> {
39
+ return { ...input };
40
+ }
41
+
42
+ function isDiagnostic(value: unknown): value is Diagnostic {
43
+ return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
44
+ }
45
+
46
+ function isRange(value: unknown): value is Diagnostic["range"] {
47
+ return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
48
+ }
49
+
50
+ function isPosition(value: unknown): value is Diagnostic["range"]["start"] {
51
+ return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
52
+ }