oh-my-opencode 5.0.0-beta.25 → 5.0.0-beta.28

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 (193) hide show
  1. package/dist/cli/config-manager/parse-opencode-config-file.d.ts +2 -1
  2. package/dist/cli/doctor/checks/system-plugin.d.ts +4 -3
  3. package/dist/cli/index.js +309 -182
  4. package/dist/cli-node/index.js +332 -182
  5. package/dist/features/team-mode/tools/messaging-fallback-wake.d.ts +15 -0
  6. package/dist/hooks/todo-continuation-enforcer/types.d.ts +1 -0
  7. package/dist/index.js +1958 -1668
  8. package/dist/shared/index.d.ts +1 -0
  9. package/dist/shared/legacy-plugin-warning.d.ts +2 -1
  10. package/dist/shared/plugin-entry-migrator.d.ts +4 -3
  11. package/dist/shared/plugin-entry-shape.d.ts +6 -0
  12. package/dist/skills/data-scientist/SKILL.md +99 -239
  13. package/dist/skills/data-scientist/references/execution-surfaces.md +91 -0
  14. package/dist/skills/data-scientist/references/placement.md +74 -0
  15. package/dist/skills/data-scientist/references/polars-lane.md +95 -0
  16. package/dist/skills/data-scientist/references/uv-setup.md +1 -1
  17. package/dist/skills/data-scientist/references/visualization.md +64 -0
  18. package/dist/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  19. package/dist/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  20. package/dist/skills/debugging/SKILL.md +2 -0
  21. package/dist/skills/debugging/references/methodology/00-setup.md +12 -0
  22. package/dist/skills/debugging/references/methodology/02-investigate.md +21 -0
  23. package/dist/skills/debugging/references/runtimes/go.md +15 -1
  24. package/dist/skills/debugging/references/runtimes/native-binary.md +38 -0
  25. package/dist/skills/debugging/references/runtimes/node.md +25 -0
  26. package/dist/skills/debugging/references/runtimes/python.md +13 -0
  27. package/dist/skills/debugging/references/runtimes/rust.md +41 -0
  28. package/dist/skills/debugging/references/scripts/dap.mjs +267 -0
  29. package/dist/skills/debugging/references/scripts/dap.test.ts +86 -0
  30. package/dist/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  31. package/dist/skills/debugging/references/tools/dap.md +103 -0
  32. package/dist/skills/debugging/references/tools/frida.md +193 -0
  33. package/dist/skills/ulw-research/SKILL.md +3 -1
  34. package/dist/tools/delegate-task/types.d.ts +3 -0
  35. package/dist/tui.d.ts +12 -0
  36. package/dist/tui.js +106 -15
  37. package/package.json +18 -15
  38. package/packages/lsp-core/src/index.ts +1 -0
  39. package/packages/lsp-core/src/lsp/client-wrapper.ts +25 -5
  40. package/packages/lsp-core/src/lsp/client.ts +26 -0
  41. package/packages/lsp-core/src/lsp/connection.ts +16 -0
  42. package/packages/lsp-core/src/lsp/constants.ts +1 -0
  43. package/packages/lsp-core/src/lsp/format-document.test.ts +143 -0
  44. package/packages/lsp-core/src/lsp/format-document.ts +90 -0
  45. package/packages/lsp-core/src/lsp/manager-max-clients.test.ts +151 -0
  46. package/packages/lsp-core/src/lsp/manager.ts +34 -1
  47. package/packages/lsp-core/src/lsp/server-definitions.ts +29 -0
  48. package/packages/lsp-core/src/lsp/server-installation.test.ts +376 -0
  49. package/packages/lsp-core/src/lsp/server-installation.ts +200 -27
  50. package/packages/lsp-core/src/lsp/server-resolution-local-binary.test.ts +170 -0
  51. package/packages/lsp-core/src/lsp/server-resolution.ts +18 -5
  52. package/packages/lsp-core/src/lsp/transport.ts +9 -0
  53. package/packages/lsp-core/src/lsp/types.ts +8 -0
  54. package/packages/lsp-core/src/request-context.test.ts +29 -0
  55. package/packages/lsp-core/src/request-context.ts +1 -1
  56. package/packages/lsp-core/src/tool-surface.test.ts +13 -1
  57. package/packages/lsp-core/src/tools/definitions.ts +14 -0
  58. package/packages/lsp-core/src/tools/format.test.ts +56 -0
  59. package/packages/lsp-core/src/tools/format.ts +56 -0
  60. package/packages/lsp-core/src/tools/index.ts +1 -0
  61. package/packages/lsp-core/src/tools/types.ts +10 -0
  62. package/packages/lsp-daemon/dist/cli.js +433 -145
  63. package/packages/lsp-daemon/dist/client.d.ts +10 -0
  64. package/packages/lsp-daemon/dist/client.js +402 -108
  65. package/packages/lsp-daemon/dist/daemon-client.d.ts +1 -0
  66. package/packages/lsp-daemon/dist/daemon-client.js +3 -0
  67. package/packages/lsp-daemon/dist/index.js +396 -108
  68. package/packages/lsp-tools-mcp/dist/cli.js +385 -100
  69. package/packages/lsp-tools-mcp/dist/lsp/manager.js +53 -0
  70. package/packages/lsp-tools-mcp/dist/mcp.js +385 -100
  71. package/packages/lsp-tools-mcp/dist/request-context.js +1 -1
  72. package/packages/lsp-tools-mcp/dist/tools.js +386 -100
  73. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  74. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
  75. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +69 -35
  78. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +21 -2
  79. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  81. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  82. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  83. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  84. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  85. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  87. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +404 -121
  88. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  89. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  90. package/packages/omo-codex/plugin/components/rules/dist/cli.js +3 -3
  91. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  92. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  93. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +3 -3
  94. package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +4 -4
  95. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  96. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  97. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +30 -15
  98. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +30 -15
  99. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  100. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  101. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  102. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  103. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  104. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  106. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  121. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  122. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  123. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  124. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  125. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  126. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  127. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  128. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  129. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  130. package/packages/omo-codex/plugin/model-catalog.json +3 -3
  131. package/packages/omo-codex/plugin/package-lock.json +13 -13
  132. package/packages/omo-codex/plugin/package.json +1 -1
  133. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +3 -3
  134. package/packages/omo-codex/plugin/skills/data-scientist/SKILL.md +99 -239
  135. package/packages/omo-codex/plugin/skills/data-scientist/references/execution-surfaces.md +91 -0
  136. package/packages/omo-codex/plugin/skills/data-scientist/references/placement.md +74 -0
  137. package/packages/omo-codex/plugin/skills/data-scientist/references/polars-lane.md +95 -0
  138. package/packages/omo-codex/plugin/skills/data-scientist/references/uv-setup.md +1 -1
  139. package/packages/omo-codex/plugin/skills/data-scientist/references/visualization.md +64 -0
  140. package/packages/omo-codex/plugin/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  141. package/packages/omo-codex/plugin/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  142. package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -0
  143. package/packages/omo-codex/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
  144. package/packages/omo-codex/plugin/skills/debugging/references/methodology/02-investigate.md +21 -0
  145. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/go.md +15 -1
  146. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
  147. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/node.md +25 -0
  148. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/python.md +13 -0
  149. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/rust.md +41 -0
  150. package/packages/omo-codex/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
  151. package/packages/omo-codex/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  152. package/packages/omo-codex/plugin/skills/debugging/references/tools/dap.md +103 -0
  153. package/packages/omo-codex/plugin/skills/debugging/references/tools/frida.md +193 -0
  154. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -1
  155. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +1 -1
  156. package/packages/omo-codex/plugin/test/auto-update.test.mjs +2 -2
  157. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +8 -8
  158. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +2 -2
  159. package/packages/omo-codex/scripts/install-dist/install-local.mjs +38 -23
  160. package/packages/shared-skills/skills/data-scientist/SKILL.md +99 -239
  161. package/packages/shared-skills/skills/data-scientist/references/execution-surfaces.md +91 -0
  162. package/packages/shared-skills/skills/data-scientist/references/placement.md +74 -0
  163. package/packages/shared-skills/skills/data-scientist/references/polars-lane.md +95 -0
  164. package/packages/shared-skills/skills/data-scientist/references/uv-setup.md +1 -1
  165. package/packages/shared-skills/skills/data-scientist/references/visualization.md +64 -0
  166. package/packages/shared-skills/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  167. package/packages/shared-skills/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  168. package/packages/shared-skills/skills/debugging/SKILL.md +2 -0
  169. package/packages/shared-skills/skills/debugging/references/methodology/00-setup.md +12 -0
  170. package/packages/shared-skills/skills/debugging/references/methodology/02-investigate.md +21 -0
  171. package/packages/shared-skills/skills/debugging/references/runtimes/go.md +15 -1
  172. package/packages/shared-skills/skills/debugging/references/runtimes/native-binary.md +38 -0
  173. package/packages/shared-skills/skills/debugging/references/runtimes/node.md +25 -0
  174. package/packages/shared-skills/skills/debugging/references/runtimes/python.md +13 -0
  175. package/packages/shared-skills/skills/debugging/references/runtimes/rust.md +41 -0
  176. package/packages/shared-skills/skills/debugging/references/scripts/dap.mjs +267 -0
  177. package/packages/shared-skills/skills/debugging/references/scripts/dap.test.ts +86 -0
  178. package/packages/shared-skills/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  179. package/packages/shared-skills/skills/debugging/references/tools/dap.md +103 -0
  180. package/packages/shared-skills/skills/debugging/references/tools/frida.md +193 -0
  181. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -1
  182. package/dist/skills/data-scientist/references/common-scenarios.md +0 -176
  183. package/dist/skills/data-scientist/references/execution-templates.md +0 -197
  184. package/dist/skills/data-scientist/references/integration-patterns.md +0 -153
  185. package/dist/skills/data-scientist/references/performance-benchmarks.md +0 -37
  186. package/packages/omo-codex/plugin/skills/data-scientist/references/common-scenarios.md +0 -176
  187. package/packages/omo-codex/plugin/skills/data-scientist/references/execution-templates.md +0 -197
  188. package/packages/omo-codex/plugin/skills/data-scientist/references/integration-patterns.md +0 -153
  189. package/packages/omo-codex/plugin/skills/data-scientist/references/performance-benchmarks.md +0 -37
  190. package/packages/shared-skills/skills/data-scientist/references/common-scenarios.md +0 -176
  191. package/packages/shared-skills/skills/data-scientist/references/execution-templates.md +0 -197
  192. package/packages/shared-skills/skills/data-scientist/references/integration-patterns.md +0 -153
  193. package/packages/shared-skills/skills/data-scientist/references/performance-benchmarks.md +0 -37
@@ -0,0 +1,90 @@
1
+ import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+
3
+ import type { LspClient } from "./client.js";
4
+ import type { FormattingOptions, TextEdit } from "./types.js";
5
+ import { normalizeTextEdits } from "./workspace-edit-text.js";
6
+
7
+ export const DEFAULT_FORMATTING_OPTIONS: FormattingOptions = {
8
+ tabSize: 4,
9
+ insertSpaces: false,
10
+ trimTrailingWhitespace: true,
11
+ insertFinalNewline: true,
12
+ trimFinalNewlines: true,
13
+ };
14
+
15
+ export type FormatDocumentResult =
16
+ | { readonly status: "formatted"; readonly linesAdded: number; readonly linesRemoved: number }
17
+ | { readonly status: "unchanged"; readonly linesAdded: 0; readonly linesRemoved: 0 }
18
+ | { readonly status: "unavailable"; readonly reason: "capability_not_advertised" };
19
+
20
+ export interface FormatDocumentOptions {
21
+ readonly formattingOptions?: FormattingOptions;
22
+ readonly signal?: AbortSignal;
23
+ }
24
+
25
+ const UNCHANGED: FormatDocumentResult = { status: "unchanged", linesAdded: 0, linesRemoved: 0 };
26
+
27
+ /**
28
+ * Formats one document through the resident language server and commits the result to disk.
29
+ *
30
+ * The server is asked for whole-document edits rather than formatted text, so the edits are
31
+ * validated and applied with the same normalization the workspace-edit path uses. Nothing is
32
+ * written when the server declines the capability or when the edits reproduce the current bytes,
33
+ * which keeps a no-op format from touching file mtimes that downstream tooling watches.
34
+ */
35
+ export async function formatDocumentWithClient(
36
+ client: LspClient,
37
+ filePath: string,
38
+ options: FormatDocumentOptions = {},
39
+ ): Promise<FormatDocumentResult> {
40
+ const edits = await client.formatDocument(
41
+ filePath,
42
+ options.formattingOptions ?? DEFAULT_FORMATTING_OPTIONS,
43
+ options.signal,
44
+ );
45
+ if (edits === null) return { status: "unavailable", reason: "capability_not_advertised" };
46
+ if (edits.length === 0) return UNCHANGED;
47
+
48
+ const before = readFileSync(filePath, "utf-8");
49
+ const normalized = normalizeTextEdits(before, edits, 0);
50
+ if (normalized.text === before) return UNCHANGED;
51
+
52
+ writeAtomically(filePath, normalized.text);
53
+ await client.openFile(filePath);
54
+
55
+ return {
56
+ status: "formatted",
57
+ ...lineDelta(normalized.edits),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Counts the lines each edit removes from the document and adds back in its place.
63
+ *
64
+ * Counting from the edits instead of diffing the whole file keeps the report proportional to what
65
+ * the server actually rewrote, so an unrelated line never shows up as churn.
66
+ */
67
+ function lineDelta(edits: readonly TextEdit[]): { readonly linesAdded: number; readonly linesRemoved: number } {
68
+ let linesAdded = 0;
69
+ let linesRemoved = 0;
70
+ for (const edit of edits) {
71
+ linesRemoved += edit.range.end.line - edit.range.start.line + 1;
72
+ linesAdded += edit.newText.split("\n").length;
73
+ }
74
+ return { linesAdded, linesRemoved };
75
+ }
76
+
77
+ function writeAtomically(filePath: string, content: string): void {
78
+ const tempPath = `${filePath}.omo-format.tmp`;
79
+ writeFileSync(tempPath, content, "utf-8");
80
+ try {
81
+ renameSync(tempPath, filePath);
82
+ } catch (error) {
83
+ try {
84
+ unlinkSync(tempPath);
85
+ } catch {
86
+ // The rename failure is the actionable error; a leftover temp file must not mask it.
87
+ }
88
+ throw error;
89
+ }
90
+ }
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import type { LspClient } from "./client.js";
4
+ import { LspManager } from "./manager.js";
5
+ import type { ResolvedServer } from "./types.js";
6
+
7
+ const SERVER: ResolvedServer = {
8
+ id: "typescript",
9
+ command: ["typescript-language-server", "--stdio"],
10
+ extensions: [".ts"],
11
+ priority: 1,
12
+ };
13
+
14
+ class FakeLspClient {
15
+ alive = true;
16
+ stopCallCount = 0;
17
+
18
+ constructor(readonly root: string) {}
19
+
20
+ async start(): Promise<void> {}
21
+
22
+ async initialize(): Promise<void> {}
23
+
24
+ isAlive(): boolean {
25
+ return this.alive;
26
+ }
27
+
28
+ command(): string[] {
29
+ return [...SERVER.command];
30
+ }
31
+
32
+ async stop(): Promise<void> {
33
+ this.stopCallCount += 1;
34
+ this.alive = false;
35
+ }
36
+ }
37
+
38
+ function managerWithFakes(maxResidentClients: number): {
39
+ readonly manager: LspManager;
40
+ readonly clients: Map<string, FakeLspClient>;
41
+ readonly clock: { value: number };
42
+ } {
43
+ const clients = new Map<string, FakeLspClient>();
44
+ const clock = { value: 1_000 };
45
+ const manager = new LspManager({
46
+ maxResidentClients,
47
+ reaperIntervalMs: 60_000,
48
+ idleTimeoutMs: 60_000_000,
49
+ now: () => clock.value,
50
+ clientFactory: (root) => {
51
+ const client = new FakeLspClient(root);
52
+ clients.set(root, client);
53
+ return client as unknown as LspClient;
54
+ },
55
+ });
56
+ return { manager, clients, clock };
57
+ }
58
+
59
+ describe("LspManager resident client cap", () => {
60
+ it("#given a cap of two idle clients #when a third client is admitted #then the least recently used one is stopped and evicted", async () => {
61
+ // given
62
+ const { manager, clients, clock } = managerWithFakes(2);
63
+ try {
64
+ await manager.getClient("/root-a", SERVER);
65
+ manager.releaseClient("/root-a", SERVER.id);
66
+ clock.value += 10;
67
+ await manager.getClient("/root-b", SERVER);
68
+ manager.releaseClient("/root-b", SERVER.id);
69
+ clock.value += 10;
70
+
71
+ // when
72
+ await manager.getClient("/root-c", SERVER);
73
+ manager.releaseClient("/root-c", SERVER.id);
74
+
75
+ // then
76
+ expect(manager.clientCount()).toBe(2);
77
+ expect(manager.hasClient("/root-a", SERVER.id)).toBe(false);
78
+ expect(manager.hasClient("/root-b", SERVER.id)).toBe(true);
79
+ expect(manager.hasClient("/root-c", SERVER.id)).toBe(true);
80
+ expect(clients.get("/root-a")?.stopCallCount).toBe(1);
81
+ } finally {
82
+ await manager.stopAll();
83
+ }
84
+ });
85
+
86
+ it("#given the least recently used client is mid-request #when a new client is admitted #then the busy client survives and an idle one is evicted", async () => {
87
+ // given
88
+ const { manager, clients, clock } = managerWithFakes(2);
89
+ try {
90
+ await manager.getClient("/root-busy", SERVER);
91
+ clock.value += 10;
92
+ await manager.getClient("/root-idle", SERVER);
93
+ manager.releaseClient("/root-idle", SERVER.id);
94
+ clock.value += 10;
95
+
96
+ // when
97
+ await manager.getClient("/root-new", SERVER);
98
+ manager.releaseClient("/root-new", SERVER.id);
99
+
100
+ // then
101
+ expect(manager.hasClient("/root-busy", SERVER.id)).toBe(true);
102
+ expect(manager.hasClient("/root-idle", SERVER.id)).toBe(false);
103
+ expect(clients.get("/root-busy")?.stopCallCount).toBe(0);
104
+ expect(clients.get("/root-idle")?.stopCallCount).toBe(1);
105
+ } finally {
106
+ manager.releaseClient("/root-busy", SERVER.id);
107
+ await manager.stopAll();
108
+ }
109
+ });
110
+
111
+ it("#given every resident client is busy #when a new client is admitted #then nothing is evicted and the cap is exceeded rather than breaking a live request", async () => {
112
+ // given
113
+ const { manager, clients } = managerWithFakes(1);
114
+ try {
115
+ await manager.getClient("/root-busy", SERVER);
116
+
117
+ // when
118
+ await manager.getClient("/root-second", SERVER);
119
+
120
+ // then
121
+ expect(manager.clientCount()).toBe(2);
122
+ expect(clients.get("/root-busy")?.stopCallCount).toBe(0);
123
+ } finally {
124
+ manager.releaseClient("/root-busy", SERVER.id);
125
+ manager.releaseClient("/root-second", SERVER.id);
126
+ await manager.stopAll();
127
+ }
128
+ });
129
+
130
+ it("#given warmup requests beyond the cap #when a warm client is admitted #then the idle least recently used client is evicted too", async () => {
131
+ // given
132
+ const { manager, clients, clock } = managerWithFakes(1);
133
+ try {
134
+ await manager.getClient("/root-first", SERVER);
135
+ manager.releaseClient("/root-first", SERVER.id);
136
+ clock.value += 10;
137
+
138
+ // when
139
+ manager.warmupClient("/root-warm", SERVER);
140
+ await Promise.resolve();
141
+ await Promise.resolve();
142
+
143
+ // then
144
+ expect(manager.hasClient("/root-first", SERVER.id)).toBe(false);
145
+ expect(clients.get("/root-first")?.stopCallCount).toBe(1);
146
+ expect(manager.hasClient("/root-warm", SERVER.id)).toBe(true);
147
+ } finally {
148
+ await manager.stopAll();
149
+ }
150
+ });
151
+ });
@@ -1,6 +1,6 @@
1
1
  import { reportBestEffortCleanupError } from "./cleanup-errors.js";
2
2
  import { LspClient } from "./client.js";
3
- import { IDLE_TIMEOUT_MS, INIT_TIMEOUT_MS, REAPER_INTERVAL_MS } from "./constants.js";
3
+ import { IDLE_TIMEOUT_MS, INIT_TIMEOUT_MS, MAX_RESIDENT_CLIENTS, REAPER_INTERVAL_MS } from "./constants.js";
4
4
  import { installProcessSignalCleanup } from "./process-signal-cleanup.js";
5
5
  import type { ResolvedServer } from "./types.js";
6
6
 
@@ -28,6 +28,7 @@ export interface ClientSnapshot {
28
28
  export interface LspManagerOptions {
29
29
  idleTimeoutMs?: number;
30
30
  initTimeoutMs?: number;
31
+ maxResidentClients?: number;
31
32
  reaperIntervalMs?: number;
32
33
  clientFactory?: (root: string, server: ResolvedServer) => LspClient;
33
34
  now?: () => number;
@@ -80,6 +81,7 @@ export class LspManager {
80
81
 
81
82
  private readonly idleTimeoutMs: number;
82
83
  private readonly initTimeoutMs: number;
84
+ private readonly maxResidentClients: number;
83
85
  private readonly reaperIntervalMs: number;
84
86
  private readonly clientFactory: (root: string, server: ResolvedServer) => LspClient;
85
87
  private readonly now: () => number;
@@ -87,6 +89,7 @@ export class LspManager {
87
89
  constructor(options: LspManagerOptions = {}) {
88
90
  this.idleTimeoutMs = options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
89
91
  this.initTimeoutMs = options.initTimeoutMs ?? INIT_TIMEOUT_MS;
92
+ this.maxResidentClients = options.maxResidentClients ?? MAX_RESIDENT_CLIENTS;
90
93
  this.reaperIntervalMs = options.reaperIntervalMs ?? REAPER_INTERVAL_MS;
91
94
  this.clientFactory = options.clientFactory ?? ((root, server) => new LspClient(root, server));
92
95
  this.now = options.now ?? (() => Date.now());
@@ -134,6 +137,32 @@ export class LspManager {
134
137
  }
135
138
  }
136
139
 
140
+ /**
141
+ * Frees capacity for one new client by stopping the least recently used idle client.
142
+ *
143
+ * Only clients with no in-flight work are eligible, so a resident server is never torn down
144
+ * underneath a live request; when every resident client is busy the cap is exceeded instead.
145
+ */
146
+ private evictForAdmission(): void {
147
+ while (this.clients.size >= this.maxResidentClients) {
148
+ const victim = this.leastRecentlyUsedIdleClient();
149
+ if (!victim) return;
150
+ this.clients.delete(victim.key);
151
+ void stopClientBestEffort(victim.managed.client);
152
+ }
153
+ }
154
+
155
+ private leastRecentlyUsedIdleClient(): { readonly key: string; readonly managed: ManagedClient } | null {
156
+ let candidate: { readonly key: string; readonly managed: ManagedClient } | null = null;
157
+ for (const [key, managed] of this.clients) {
158
+ if (managed.refCount > 0 || managed.pendingWaiters > 0 || managed.isInitializing) continue;
159
+ if (candidate === null || managed.lastUsedAt < candidate.managed.lastUsedAt) {
160
+ candidate = { key, managed };
161
+ }
162
+ }
163
+ return candidate;
164
+ }
165
+
137
166
  private async tryDeleteIfOrphaned(key: string, managed: ManagedClient): Promise<void> {
138
167
  if (
139
168
  managed.refCount === 0 &&
@@ -197,6 +226,8 @@ export class LspManager {
197
226
  return managed.client;
198
227
  }
199
228
 
229
+ this.evictForAdmission();
230
+
200
231
  const client = this.clientFactory(root, server);
201
232
  const initStartedAt = this.now();
202
233
  const initPromise = (async () => {
@@ -264,6 +295,8 @@ export class LspManager {
264
295
  const key = this.getKey(root, server.id);
265
296
  if (this.clients.has(key)) return;
266
297
 
298
+ this.evictForAdmission();
299
+
267
300
  const client = this.clientFactory(root, server);
268
301
  const initStartedAt = this.now();
269
302
  const initPromise = (async () => {
@@ -49,6 +49,35 @@ export const LSP_INSTALL_HINTS: Record<string, string> = {
49
49
  "Install: dotnet tool install -g roslyn-language-server --prerelease (requires v5.8.0+). See https://github.com/dotnet/razor",
50
50
  };
51
51
 
52
+ /**
53
+ * Repo-local install commands, offered ahead of the global hints in `LSP_INSTALL_HINTS`.
54
+ *
55
+ * A devDependency (or project venv) is resolvable by `resolveServerBinary()` without any
56
+ * global install, and it pins the version the project actually expects. Only servers that
57
+ * are genuinely distributed per-project belong here; toolchain-wide servers (rust-analyzer,
58
+ * gopls, clangd, ...) keep their global hint alone.
59
+ */
60
+ export const LSP_LOCAL_INSTALL_HINTS: Record<string, string> = {
61
+ typescript: "bun add -d typescript-language-server typescript",
62
+ vue: "bun add -d @vue/language-server",
63
+ eslint: "bun add -d vscode-langservers-extracted",
64
+ oxlint: "bun add -d oxlint",
65
+ biome: "bun add -d @biomejs/biome",
66
+ svelte: "bun add -d svelte-language-server",
67
+ astro: "bun add -d @astrojs/language-server",
68
+ bash: "bun add -d bash-language-server",
69
+ "bash-ls": "bun add -d bash-language-server",
70
+ "yaml-ls": "bun add -d yaml-language-server",
71
+ php: "bun add -d intelephense",
72
+ prisma: "bun add -d prisma",
73
+ dockerfile: "bun add -d dockerfile-language-server-nodejs",
74
+ pyright: "uv add --dev pyright",
75
+ basedpyright: "uv add --dev basedpyright",
76
+ ruff: "uv add --dev ruff",
77
+ ty: "uv add --dev ty",
78
+ "ruby-lsp": "bundle add ruby-lsp --group development",
79
+ };
80
+
52
81
  export const BUILTIN_SERVERS: Record<string, Omit<LspServerConfig, "id">> = {
53
82
  typescript: {
54
83
  command: ["typescript-language-server", "--stdio"],