oh-my-opencode 4.18.0 → 4.18.2

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 (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -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/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { spawnSync } from "node:child_process";
4
+ import { builtinModules } from "node:module";
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ renameSync,
11
+ rmSync,
12
+ statSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { dirname, join, relative } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const SCHEMA_VERSION = 1;
19
+ const componentRoot = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const distDir = join(componentRoot, "dist");
21
+ const manifestPath = join(distDir, ".omo-runtime-manifest.json");
22
+ const builtinSpecifiers = new Set([
23
+ ...builtinModules,
24
+ ...builtinModules.map((name) => `node:${name}`),
25
+ ]);
26
+
27
+ if (!existsSync(join(componentRoot, "src"))) {
28
+ validateDistOnlyManifest();
29
+ process.exit(0);
30
+ }
31
+
32
+ if (isManifestFresh()) {
33
+ process.exit(0);
34
+ }
35
+
36
+ rmSync(distDir, { recursive: true, force: true });
37
+ mkdirSync(distDir, { recursive: true });
38
+ run("tsc", ["-p", "tsconfig.build.json"]);
39
+ run("bun", [
40
+ "build",
41
+ "src/cli.ts",
42
+ "--target",
43
+ "node",
44
+ "--format",
45
+ "esm",
46
+ "--outfile",
47
+ "dist/cli.js",
48
+ ]);
49
+ assertBuiltinOnlyCli();
50
+ writeManifest();
51
+ validateManifest({ requireFreshInputs: true });
52
+
53
+ function run(command, args) {
54
+ const result = spawnSync(command, args, {
55
+ cwd: componentRoot,
56
+ shell: process.platform === "win32",
57
+ stdio: "inherit",
58
+ });
59
+ if (result.error !== undefined) throw result.error;
60
+ if (result.status !== 0) process.exit(result.status ?? 1);
61
+ }
62
+
63
+ function isManifestFresh() {
64
+ try {
65
+ validateManifest({ requireFreshInputs: true });
66
+ return true;
67
+ } catch (error) {
68
+ if (error instanceof Error) return false;
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ function validateDistOnlyManifest() {
74
+ try {
75
+ validateManifest({ requireFreshInputs: false });
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ console.error(`Invalid installed codex-lsp runtime manifest: ${message}`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ function validateManifest({ requireFreshInputs }) {
84
+ const manifest = readManifest();
85
+ if (manifest.schemaVersion !== SCHEMA_VERSION) throw new Error("unsupported manifest schemaVersion");
86
+ if (manifest.version !== packageVersion()) throw new Error("manifest version is stale");
87
+ if (requireFreshInputs && manifest.inputDigest !== inputDigest()) throw new Error("manifest inputDigest is stale");
88
+ const outputs = [...manifest.outputs].sort((left, right) => left.path.localeCompare(right.path));
89
+ if (JSON.stringify(outputs) !== JSON.stringify(manifest.outputs)) throw new Error("manifest outputs are not sorted");
90
+ for (const output of outputs) {
91
+ const outputPath = join(distDir, output.path);
92
+ if (!existsSync(outputPath)) throw new Error(`manifest output is missing: ${output.path}`);
93
+ if (sha256File(outputPath) !== output.sha256) throw new Error(`manifest output hash mismatch: ${output.path}`);
94
+ }
95
+ }
96
+
97
+ function readManifest() {
98
+ if (!existsSync(manifestPath)) throw new Error("manifest is missing");
99
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
100
+ if (!isRecord(parsed)) throw new Error("manifest must be an object");
101
+ if (parsed.schemaVersion !== SCHEMA_VERSION) throw new Error("manifest schemaVersion must be 1");
102
+ if (typeof parsed.version !== "string") throw new Error("manifest version must be a string");
103
+ if (typeof parsed.inputDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(parsed.inputDigest)) {
104
+ throw new Error("manifest inputDigest must be sha256:<64hex>");
105
+ }
106
+ if (!Array.isArray(parsed.outputs) || !parsed.outputs.every(isManifestOutput)) {
107
+ throw new Error("manifest outputs are malformed");
108
+ }
109
+ return parsed;
110
+ }
111
+
112
+ function isManifestOutput(value) {
113
+ return (
114
+ isRecord(value) &&
115
+ typeof value.path === "string" &&
116
+ value.path.length > 0 &&
117
+ !value.path.includes("..") &&
118
+ typeof value.sha256 === "string" &&
119
+ /^[0-9a-f]{64}$/.test(value.sha256)
120
+ );
121
+ }
122
+
123
+ function writeManifest() {
124
+ const manifest = {
125
+ schemaVersion: SCHEMA_VERSION,
126
+ version: packageVersion(),
127
+ inputDigest: inputDigest(),
128
+ outputs: outputFiles().map((path) => ({ path, sha256: sha256File(join(distDir, path)) })),
129
+ };
130
+ manifest.outputs.sort((left, right) => left.path.localeCompare(right.path));
131
+ const tempPath = join(distDir, `.omo-runtime-manifest.${process.pid}.tmp`);
132
+ writeFileSync(tempPath, `${JSON.stringify(manifest, null, 2)}\n`);
133
+ renameSync(tempPath, manifestPath);
134
+ }
135
+
136
+ function inputDigest() {
137
+ const hash = createHash("sha256");
138
+ for (const path of inputFiles()) {
139
+ hash.update(path);
140
+ hash.update("\0");
141
+ hash.update(sha256File(join(componentRoot, path)));
142
+ hash.update("\0");
143
+ }
144
+ return `sha256:${hash.digest("hex")}`;
145
+ }
146
+
147
+ function inputFiles() {
148
+ return [
149
+ "package.json",
150
+ "tsconfig.json",
151
+ "tsconfig.build.json",
152
+ "scripts/build-runtime.mjs",
153
+ ...walk(join(componentRoot, "src")).map((path) => relative(componentRoot, path)),
154
+ ].sort();
155
+ }
156
+
157
+ function outputFiles() {
158
+ return walk(distDir)
159
+ .map((path) => relative(distDir, path))
160
+ .filter((path) => path !== ".omo-runtime-manifest.json")
161
+ .sort();
162
+ }
163
+
164
+ function walk(root) {
165
+ const entries = [];
166
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
167
+ const path = join(root, entry.name);
168
+ if (entry.isDirectory()) {
169
+ entries.push(...walk(path));
170
+ } else if (entry.isFile()) {
171
+ entries.push(path);
172
+ }
173
+ }
174
+ return entries;
175
+ }
176
+
177
+ function assertBuiltinOnlyCli() {
178
+ const source = readFileSync(join(distDir, "cli.js"), "utf8");
179
+ const specifiers = [
180
+ ...source.matchAll(/\bimport\s+(?:[^'";]+?\s+from\s+)?["']([^"']+)["']/g),
181
+ ...source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g),
182
+ ].map((match) => match[1]);
183
+ const invalid = specifiers.filter((specifier) => !builtinSpecifiers.has(specifier));
184
+ if (invalid.length > 0) {
185
+ throw new Error(`dist/cli.js has non-builtin runtime imports: ${invalid.join(", ")}`);
186
+ }
187
+ }
188
+
189
+ function packageVersion() {
190
+ const parsed = JSON.parse(readFileSync(join(componentRoot, "package.json"), "utf8"));
191
+ if (!isRecord(parsed) || typeof parsed.version !== "string") throw new Error("package.json version is missing");
192
+ return parsed.version;
193
+ }
194
+
195
+ function sha256File(path) {
196
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
197
+ }
198
+
199
+ function isRecord(value) {
200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
201
+ }
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+
8
+ const SCRIPT_URL = new URL("./build-runtime.mjs", import.meta.url);
9
+
10
+ test("#given a built component runtime #when the manifest is inspected #then it pins sorted output hashes", async () => {
11
+ const manifest = JSON.parse(await readFile(new URL("../dist/.omo-runtime-manifest.json", import.meta.url), "utf8"));
12
+ assert.equal(manifest.schemaVersion, 1);
13
+ assert.match(manifest.inputDigest, /^sha256:[0-9a-f]{64}$/u);
14
+ assert.deepEqual(
15
+ manifest.outputs.map((output) => output.path),
16
+ [...manifest.outputs.map((output) => output.path)].sort(),
17
+ );
18
+ assert(manifest.outputs.some((output) => output.path === "cli.js"));
19
+ assert(manifest.outputs.every((output) => /^[0-9a-f]{64}$/u.test(output.sha256)));
20
+ });
21
+
22
+ test("#given a dist-only installed component #when output bytes are tampered #then validation fails actionably", async () => {
23
+ const root = await mkdtemp(join(tmpdir(), "codex-lsp-dist-only-"));
24
+ try {
25
+ const componentRoot = join(root, "component");
26
+ await mkdir(join(componentRoot, "scripts"), { recursive: true });
27
+ await copyFile(SCRIPT_URL, join(componentRoot, "scripts", "build-runtime.mjs"));
28
+ await writeFile(join(componentRoot, "package.json"), JSON.stringify({ version: "4.17.0" }));
29
+ await mkdir(join(componentRoot, "dist"), { recursive: true });
30
+ await writeFile(join(componentRoot, "dist", "cli.js"), "console.log('tampered')\n");
31
+ await writeFile(
32
+ join(componentRoot, "dist", ".omo-runtime-manifest.json"),
33
+ JSON.stringify(
34
+ {
35
+ schemaVersion: 1,
36
+ version: "4.17.0",
37
+ inputDigest: `sha256:${"0".repeat(64)}`,
38
+ outputs: [{ path: "cli.js", sha256: "1".repeat(64) }],
39
+ },
40
+ null,
41
+ 2,
42
+ ),
43
+ );
44
+
45
+ const result = spawnSync(process.execPath, [join(componentRoot, "scripts", "build-runtime.mjs")], {
46
+ encoding: "utf8",
47
+ });
48
+
49
+ assert.notEqual(result.status, 0);
50
+ assert.match(result.stderr, /Invalid installed codex-lsp runtime manifest/u);
51
+ assert.match(result.stderr, /hash mismatch/u);
52
+ } finally {
53
+ await rm(root, { recursive: true, force: true });
54
+ }
55
+ });
@@ -1,7 +1,5 @@
1
1
  import { stdin as processStdin } from "node:process";
2
2
 
3
- import { disposeDefaultLspManager } from "@code-yeongyu/lsp-daemon";
4
-
5
3
  import { isRecord, runLspPostCompactHook, runLspPostToolUseHook } from "./codex-hook.js";
6
4
 
7
5
  export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
@@ -26,8 +24,6 @@ async function runHookCli(
26
24
  } catch {
27
25
  // LSP feedback is best-effort: stderr or a non-zero exit here surfaces as harness noise on every edit.
28
26
  return;
29
- } finally {
30
- await disposeDefaultLspManager().catch(() => undefined);
31
27
  }
32
28
  }
33
29
 
@@ -1,21 +1,24 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
2
4
 
3
- import { callDiagnosticsViaDaemon, currentRequestContext } from "@code-yeongyu/lsp-daemon";
5
+ import { callDiagnosticsViaDaemon } from "@code-yeongyu/lsp-daemon/client";
6
+ import { collectPostEditDiagnostics, type PostEditDiagnosticsOutcome } from "@oh-my-opencode/lsp-core/post-edit";
7
+ import { parseLspRequestContext, type LspRequestContext } from "@oh-my-opencode/lsp-core/request-context";
4
8
 
5
9
  import { ensureLspDaemonCliEnv } from "./daemon-cli-path.js";
6
10
  import {
7
11
  isLspDaemonUnreachableDiagnostics,
8
- isUnavailableLspDiagnostics,
9
12
  markLspSessionCompacted,
10
- recordLspDiagnosticsObservations,
13
+ readLspPostEditCache,
11
14
  sessionIdFrom,
12
- shouldSkipUnavailableLspDiagnostics,
15
+ writeLspPostEditCache,
13
16
  } from "./lsp-session-state.js";
14
17
  import { extractMutatedFilePaths } from "./mutated-file-paths.js";
15
18
 
16
19
  export { extractMutatedFilePaths } from "./mutated-file-paths.js";
17
20
 
18
- export type DiagnosticsRunner = (filePath: string) => Promise<string>;
21
+ export type DiagnosticsRunner = (filePath: string) => Promise<PostEditDiagnosticsOutcome>;
19
22
 
20
23
  export interface CodexPostToolUseInput {
21
24
  session_id?: unknown;
@@ -43,13 +46,10 @@ interface PostToolUseHookOutput {
43
46
  };
44
47
  }
45
48
 
46
- const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
47
- const UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
48
49
  const DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
49
50
  const DIAGNOSTIC_CHUNK_PATTERN = /^(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/;
50
51
  const DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
51
52
  const CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
52
- const MAX_CONCURRENT_DIAGNOSTICS = 4;
53
53
  const CONTEXT_PRESSURE_MARKERS = [
54
54
  "context compacted",
55
55
  "context_length_exceeded",
@@ -60,35 +60,57 @@ const CONTEXT_PRESSURE_MARKERS = [
60
60
  "long threads and multiple compactions",
61
61
  ] as const;
62
62
 
63
- export async function runLspDiagnosticsText(filePath: string): Promise<string> {
63
+ export async function runLspDiagnosticsText(filePath: string): Promise<PostEditDiagnosticsOutcome> {
64
64
  ensureLspDaemonCliEnv();
65
- const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
65
+ const result = await callDiagnosticsViaDaemon(filePath, { context: codexLspRequestContext() });
66
+ return postEditOutcomeFromDaemonResult(result);
67
+ }
68
+
69
+ function postEditOutcomeFromDaemonResult(result: {
70
+ readonly content: readonly { readonly type: string; readonly text?: string }[];
71
+ readonly details?: unknown;
72
+ }): PostEditDiagnosticsOutcome {
73
+ const availability = notConfiguredAvailability(result.details);
74
+ if (availability !== undefined) return { kind: "not_configured", extension: availability.extension };
66
75
  return result.content.map((block) => block.text).join("\n");
67
76
  }
68
77
 
78
+ function notConfiguredAvailability(details: unknown): { readonly extension: string } | undefined {
79
+ if (!isRecord(details)) return undefined;
80
+ const availability = details["availability"];
81
+ if (!isRecord(availability)) return undefined;
82
+ if (availability["kind"] !== "not_configured") return undefined;
83
+ const extension = availability["extension"];
84
+ return typeof extension === "string" && extension.length > 0 ? { extension } : undefined;
85
+ }
86
+
87
+ export function codexLspRequestContext(
88
+ env: Record<string, string | undefined> = process.env,
89
+ cwd: string = process.cwd(),
90
+ ): LspRequestContext {
91
+ const canonicalCwd = realpathSync(resolve(cwd));
92
+ const codexHome = resolve(env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"));
93
+ return parseLspRequestContext({
94
+ cwd: canonicalCwd,
95
+ projectConfigPaths: [join(canonicalCwd, ".codex", "lsp-client.json")],
96
+ userConfigPath: join(codexHome, "lsp-client.json"),
97
+ installDecisionsPath: join(codexHome, "lsp-install-decisions.json"),
98
+ capabilities: { installDecisionTool: true },
99
+ });
100
+ }
101
+
69
102
  export async function runLspPostToolUseHook(
70
103
  input: CodexPostToolUseInput,
71
104
  runDiagnostics: DiagnosticsRunner = runLspDiagnosticsText,
72
105
  ): Promise<string> {
73
106
  const sessionId = sessionIdFrom(input);
74
- const filePaths = extractMutatedFilePaths(input).filter(
75
- (filePath) => !shouldSkipUnavailableLspDiagnostics(filePath, sessionId),
76
- );
107
+ const filePaths = extractMutatedFilePaths(input);
77
108
  if (filePaths.length === 0) return "";
78
109
 
79
- const blocks: DiagnosticBlock[] = [];
80
- const observations: Array<{ filePath: string; unavailable: boolean }> = [];
81
- for (const { filePath, diagnostics } of await collectDiagnostics(filePaths, runDiagnostics)) {
82
- // A daemon outage is transient (connect-or-spawn retries on the next request);
83
- // recording it would silence this extension for the rest of the session.
84
- if (isLspDaemonUnreachableDiagnostics(diagnostics)) continue;
85
- const unavailable = isUnavailableLspDiagnostics(diagnostics);
86
- observations.push({ filePath, unavailable });
87
- if (isCleanDiagnostics(diagnostics)) continue;
88
- if (unavailable) continue;
89
- blocks.push({ filePath, diagnostics });
90
- }
91
- recordLspDiagnosticsObservations(sessionId, observations);
110
+ const cache = readLspPostEditCache(sessionId);
111
+ const result = await collectPostEditDiagnostics({ filePaths, runDiagnostics, cache });
112
+ writeLspPostEditCache(sessionId, cache);
113
+ const blocks = result.blocks.filter(({ diagnostics }) => !isLspDaemonUnreachableDiagnostics(diagnostics));
92
114
 
93
115
  if (blocks.length === 0) return "";
94
116
 
@@ -110,42 +132,6 @@ export async function runLspPostCompactHook(input: CodexPostCompactInput): Promi
110
132
  return "";
111
133
  }
112
134
 
113
- async function collectDiagnostics(
114
- filePaths: readonly string[],
115
- runDiagnostics: DiagnosticsRunner,
116
- ): Promise<DiagnosticBlock[]> {
117
- const results: DiagnosticBlock[] = [];
118
- let nextIndex = 0;
119
- const workerCount = Math.min(MAX_CONCURRENT_DIAGNOSTICS, filePaths.length);
120
- async function worker(): Promise<void> {
121
- for (;;) {
122
- const index = nextIndex;
123
- nextIndex += 1;
124
- const filePath = filePaths[index];
125
- if (filePath === undefined) return;
126
- results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
127
- }
128
- }
129
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
130
- return results;
131
- }
132
-
133
- async function collectFileDiagnostics(filePath: string, runDiagnostics: DiagnosticsRunner): Promise<string> {
134
- try {
135
- return (await runDiagnostics(filePath)).trim();
136
- } catch (error) {
137
- return formatDiagnosticsError(error);
138
- }
139
- }
140
-
141
- function formatDiagnosticsError(error: unknown): string {
142
- if (error instanceof Error) {
143
- const message = error.message.trim();
144
- if (message.length > 0) return message;
145
- }
146
- return String(error).trim();
147
- }
148
-
149
135
  function formatDiagnosticBlock({ filePath, diagnostics }: DiagnosticBlock): string {
150
136
  return `LSP diagnostics after editing ${filePath}:\n\n${formatDiagnosticsForDisplay(diagnostics)}`;
151
137
  }
@@ -221,14 +207,6 @@ function limitHookText(text: string, maxChars: number): string {
221
207
  return `${head}${marker}`;
222
208
  }
223
209
 
224
- function isCleanDiagnostics(diagnostics: string): boolean {
225
- return (
226
- diagnostics.length === 0 ||
227
- diagnostics === CLEAN_DIAGNOSTICS_TEXT ||
228
- diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT)
229
- );
230
- }
231
-
232
210
  export function isRecord(value: unknown): value is Record<string, unknown> {
233
211
  return typeof value === "object" && value !== null && !Array.isArray(value);
234
212
  }
@@ -3,28 +3,32 @@ import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
+ import {
7
+ OMO_LSP_DAEMON_CLI,
8
+ OMO_LSP_DAEMON_VERSION,
9
+ resolveDaemonRuntime,
10
+ } from "@code-yeongyu/lsp-daemon/client";
11
+
6
12
  const requireFromHere = createRequire(import.meta.url);
7
13
 
8
- const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
9
- const CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
10
- const CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
14
+ const PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/cli";
11
15
 
12
16
  interface LspDaemonCliResolution {
13
17
  cliPath: string;
14
- version: string | null;
18
+ version: string;
15
19
  }
16
20
 
17
21
  export function ensureLspDaemonCliEnv(env: NodeJS.ProcessEnv = process.env): void {
18
- const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
19
- const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
20
- if (!configuredCli) env[CODEX_LSP_DAEMON_CLI_ENV] = resolution.cliPath;
21
- if (!env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim() && resolution.version !== null) {
22
- env[CODEX_LSP_DAEMON_VERSION_ENV] = resolution.version;
23
- }
22
+ const runtime = resolveDaemonRuntime(env, resolveLspDaemonCli());
23
+ env[OMO_LSP_DAEMON_CLI] = runtime.cliPath;
24
+ env[OMO_LSP_DAEMON_VERSION] = runtime.version;
24
25
  }
25
26
 
26
- export function resolveLspDaemonCliPath(): string {
27
- return resolveLspDaemonCli().cliPath;
27
+ export function resolveLspDaemonCliPath(env: NodeJS.ProcessEnv = process.env): string {
28
+ const runtime = resolveDaemonRuntime(env, resolveLspDaemonCli());
29
+ if (env[OMO_LSP_DAEMON_CLI] === undefined) env[OMO_LSP_DAEMON_CLI] = runtime.cliPath;
30
+ if (env[OMO_LSP_DAEMON_VERSION] === undefined) env[OMO_LSP_DAEMON_VERSION] = runtime.version;
31
+ return runtime.cliPath;
28
32
  }
29
33
 
30
34
  function resolveLspDaemonCli(): LspDaemonCliResolution {
@@ -38,15 +42,20 @@ function resolveLspDaemonCli(): LspDaemonCliResolution {
38
42
  function resolvePackageLspDaemonCliPath(): string | null {
39
43
  try {
40
44
  return requireFromHere.resolve(PACKAGE_LSP_DAEMON_CLI);
41
- } catch {
45
+ } catch (error) {
46
+ if (!(error instanceof Error)) throw error;
42
47
  return null;
43
48
  }
44
49
  }
45
50
 
46
51
  function resolveConfiguredLspDaemonCli(cliPath: string): LspDaemonCliResolution {
52
+ const version = readDaemonPackageVersion(cliPath);
53
+ if (version === null) {
54
+ throw new Error(`Unable to determine packaged LSP daemon version beside ${cliPath}`);
55
+ }
47
56
  return {
48
57
  cliPath,
49
- version: readDaemonPackageVersion(cliPath),
58
+ version,
50
59
  };
51
60
  }
52
61
 
@@ -56,7 +65,9 @@ function readDaemonPackageVersion(cliPath: string): string | null {
56
65
  if (isRecord(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
57
66
  return parsed["version"];
58
67
  }
59
- } catch {}
68
+ } catch (error) {
69
+ if (!(error instanceof Error)) throw error;
70
+ }
60
71
  return null;
61
72
  }
62
73
 
@@ -1,71 +1,33 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, extname, join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
 
5
- interface LspSessionState {
6
- readonly unavailableExtensions: readonly string[];
7
- readonly postCompactProbePending?: boolean;
8
- }
5
+ import type { PostEditNotConfiguredCache } from "@oh-my-opencode/lsp-core/post-edit";
9
6
 
10
- export interface DiagnosticsObservation {
11
- readonly filePath: string;
12
- readonly unavailable: boolean;
7
+ interface LspSessionState {
8
+ readonly notConfiguredExtensions: readonly string[];
13
9
  }
14
10
 
15
11
  export function sessionIdFrom(input: { readonly session_id?: unknown }): string | undefined {
16
12
  return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
17
13
  }
18
14
 
19
- export function shouldSkipUnavailableLspDiagnostics(filePath: string, sessionId: string | undefined): boolean {
20
- if (sessionId === undefined) return false;
15
+ export function readLspPostEditCache(sessionId: string | undefined): PostEditNotConfiguredCache {
16
+ if (sessionId === undefined) return { notConfiguredExtensions: new Set() };
21
17
  const state = readSessionState(sessionStatePath(sessionId));
22
- const extension = extensionKey(filePath);
23
- return (
24
- extension !== undefined &&
25
- state.postCompactProbePending !== true &&
26
- state.unavailableExtensions.includes(extension)
27
- );
28
- }
29
-
30
- export function recordLspDiagnosticsObservations(
31
- sessionId: string | undefined,
32
- observations: readonly DiagnosticsObservation[],
33
- ): void {
34
- if (sessionId === undefined || observations.length === 0) return;
35
- const state = readSessionState(sessionStatePath(sessionId));
36
- const unavailableExtensions = new Set(state.unavailableExtensions);
37
-
38
- for (const observation of observations) {
39
- const extension = extensionKey(observation.filePath);
40
- if (extension === undefined) continue;
41
- if (observation.unavailable) {
42
- unavailableExtensions.add(extension);
43
- } else {
44
- unavailableExtensions.delete(extension);
45
- }
46
- }
47
-
48
- writeSessionState(sessionStatePath(sessionId), { unavailableExtensions: [...unavailableExtensions].sort() });
18
+ return { notConfiguredExtensions: new Set(state.notConfiguredExtensions) };
49
19
  }
50
20
 
51
- export function markLspSessionCompacted(sessionId: string | undefined): void {
21
+ export function writeLspPostEditCache(sessionId: string | undefined, cache: PostEditNotConfiguredCache): void {
52
22
  if (sessionId === undefined) return;
53
- const state = readSessionState(sessionStatePath(sessionId));
54
- if (state.unavailableExtensions.length === 0) return;
55
23
  writeSessionState(sessionStatePath(sessionId), {
56
- unavailableExtensions: state.unavailableExtensions,
57
- postCompactProbePending: true,
24
+ notConfiguredExtensions: [...cache.notConfiguredExtensions].sort(),
58
25
  });
59
26
  }
60
27
 
61
- export function isUnavailableLspDiagnostics(diagnostics: string): boolean {
62
- const normalized = diagnostics.trim();
63
- return (
64
- normalized.includes("LSP request timeout (method: initialize)") ||
65
- normalized.includes("LSP server is still initializing") ||
66
- normalized.includes("NOT INSTALLED") ||
67
- normalized.includes("Command not found:")
68
- );
28
+ export function markLspSessionCompacted(sessionId: string | undefined): void {
29
+ if (sessionId === undefined) return;
30
+ writeSessionState(sessionStatePath(sessionId), emptyState());
69
31
  }
70
32
 
71
33
  export function isLspDaemonUnreachableDiagnostics(diagnostics: string): boolean {
@@ -80,7 +42,7 @@ function sessionStatePath(sessionId: string): string {
80
42
  function readSessionState(path: string): LspSessionState {
81
43
  try {
82
44
  const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
83
- if (isLspSessionState(parsed)) return parsed;
45
+ if (isRecord(parsed) && isLspSessionState(parsed)) return normalizeSessionState(parsed);
84
46
  return emptyState();
85
47
  } catch (error) {
86
48
  if (error instanceof SyntaxError || (isRecord(error) && error["code"] === "ENOENT")) return emptyState();
@@ -94,25 +56,25 @@ function writeSessionState(path: string, state: LspSessionState): void {
94
56
  }
95
57
 
96
58
  function emptyState(): LspSessionState {
97
- return { unavailableExtensions: [] };
98
- }
99
-
100
- function extensionKey(filePath: string): string | undefined {
101
- const extension = extname(filePath).toLowerCase();
102
- return extension.length === 0 ? undefined : extension;
59
+ return { notConfiguredExtensions: [] };
103
60
  }
104
61
 
105
62
  function safePathSegment(value: string): string {
106
63
  return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
107
64
  }
108
65
 
109
- function isLspSessionState(value: unknown): value is LspSessionState {
110
- if (!isRecord(value) || !Array.isArray(value["unavailableExtensions"])) return false;
111
- const postCompactProbePending = value["postCompactProbePending"];
112
- return (
113
- value["unavailableExtensions"].every((item) => typeof item === "string") &&
114
- (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean")
115
- );
66
+ function isLspSessionState(value: Record<string, unknown>): boolean {
67
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
68
+ return Array.isArray(notConfiguredExtensions) && notConfiguredExtensions.every((item) => typeof item === "string");
69
+ }
70
+
71
+ function normalizeSessionState(value: Record<string, unknown>): LspSessionState {
72
+ const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
73
+ return {
74
+ notConfiguredExtensions: Array.isArray(notConfiguredExtensions)
75
+ ? notConfiguredExtensions.filter((item) => typeof item === "string").sort()
76
+ : [],
77
+ };
116
78
  }
117
79
 
118
80
  function isRecord(value: unknown): value is Record<string, unknown> {