gsd-pi 2.8.2 → 2.9.0

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/README.md +2 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/loader.js +1 -1
  4. package/dist/update-check.d.ts +24 -0
  5. package/dist/update-check.js +93 -0
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  49. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  51. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  57. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  59. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  61. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  62. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  63. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  64. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  65. package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  71. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  72. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  73. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  74. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  75. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  76. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  77. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  78. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  79. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  80. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  81. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  82. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  83. package/package.json +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  114. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  116. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  118. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  121. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  123. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  125. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  127. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  129. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  141. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  145. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  146. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  147. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  148. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  149. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  150. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  151. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  152. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  153. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  154. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  155. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  156. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  158. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  159. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  160. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  161. package/src/resources/extensions/ask-user-questions.ts +42 -2
  162. package/src/resources/extensions/bg-shell/index.ts +34 -37
  163. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  164. package/src/resources/extensions/browser-tools/index.ts +2 -2
  165. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  166. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  167. package/src/resources/extensions/context7/index.ts +2 -2
  168. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  169. package/src/resources/extensions/google-search/index.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +126 -12
  171. package/src/resources/extensions/gsd/commands.ts +218 -3
  172. package/src/resources/extensions/gsd/doctor.ts +1 -1
  173. package/src/resources/extensions/gsd/git-service.ts +163 -13
  174. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  175. package/src/resources/extensions/gsd/index.ts +17 -7
  176. package/src/resources/extensions/gsd/preferences.ts +1 -1
  177. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  178. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  179. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  180. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  181. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  182. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  183. package/src/resources/extensions/gsd/types.ts +1 -0
  184. package/src/resources/extensions/gsd/worktree.ts +20 -1
  185. package/src/resources/extensions/mac-tools/index.ts +1 -1
  186. package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
  187. package/src/resources/extensions/search-the-web/format.ts +1 -1
  188. package/src/resources/extensions/search-the-web/index.ts +5 -5
  189. package/src/resources/extensions/search-the-web/native-search.ts +5 -6
  190. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  191. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  192. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  193. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -0,0 +1,325 @@
1
+ import * as fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import YAML from "yaml";
7
+ import { globSync } from "glob";
8
+ import { CONFIG_DIR_NAME } from "../../config.js";
9
+ import { isRecord } from "./helpers.js";
10
+ import type { ServerConfig } from "./types.js";
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const DEFAULTS = require("./defaults.json") as Record<string, Partial<ServerConfig>>;
14
+
15
+ export interface LspConfig {
16
+ servers: Record<string, ServerConfig>;
17
+ /** Idle timeout in milliseconds. If set, LSP clients will be shutdown after this period of inactivity. Disabled by default. */
18
+ idleTimeoutMs?: number;
19
+ }
20
+
21
+ // =============================================================================
22
+ // Default Server Configuration Loading
23
+ // =============================================================================
24
+
25
+ const PID_TOKEN = "$PID";
26
+
27
+ interface NormalizedConfig {
28
+ servers: Record<string, Partial<ServerConfig>>;
29
+ idleTimeoutMs?: number;
30
+ }
31
+
32
+ function parseConfigContent(content: string, filePath: string): unknown {
33
+ const extension = path.extname(filePath).toLowerCase();
34
+ if (extension === ".yaml" || extension === ".yml") {
35
+ return YAML.parse(content) as unknown;
36
+ }
37
+ return JSON.parse(content) as unknown;
38
+ }
39
+
40
+ function normalizeConfig(value: unknown): NormalizedConfig | null {
41
+ if (!isRecord(value)) return null;
42
+
43
+ const idleTimeoutMs = typeof value.idleTimeoutMs === "number" ? value.idleTimeoutMs : undefined;
44
+ const rawServers = value.servers;
45
+
46
+ if (isRecord(rawServers)) {
47
+ return { servers: rawServers as Record<string, Partial<ServerConfig>>, idleTimeoutMs };
48
+ }
49
+
50
+ const servers = Object.fromEntries(Object.entries(value).filter(([key]) => key !== "idleTimeoutMs")) as Record<
51
+ string,
52
+ Partial<ServerConfig>
53
+ >;
54
+
55
+ return { servers, idleTimeoutMs };
56
+ }
57
+
58
+ function normalizeStringArray(value: unknown): string[] | null {
59
+ if (!Array.isArray(value)) return null;
60
+ const items = value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
61
+ return items.length > 0 ? items : null;
62
+ }
63
+
64
+ function normalizeServerConfig(name: string, config: Partial<ServerConfig>): ServerConfig | null {
65
+ const command = typeof config.command === "string" && config.command.length > 0 ? config.command : null;
66
+ const fileTypes = normalizeStringArray(config.fileTypes);
67
+ const rootMarkers = normalizeStringArray(config.rootMarkers);
68
+
69
+ if (!command || !fileTypes || !rootMarkers) {
70
+ return null;
71
+ }
72
+
73
+ const args = Array.isArray(config.args)
74
+ ? config.args.filter((entry): entry is string => typeof entry === "string")
75
+ : undefined;
76
+
77
+ return {
78
+ ...config,
79
+ command,
80
+ args,
81
+ fileTypes,
82
+ rootMarkers,
83
+ };
84
+ }
85
+
86
+ function readConfigFile(filePath: string): NormalizedConfig | null {
87
+ try {
88
+ const content = fs.readFileSync(filePath, "utf-8");
89
+ const parsed = parseConfigContent(content, filePath);
90
+ return normalizeConfig(parsed);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function coerceServerConfigs(servers: Record<string, Partial<ServerConfig>>): Record<string, ServerConfig> {
97
+ const result: Record<string, ServerConfig> = {};
98
+ for (const [name, config] of Object.entries(servers)) {
99
+ const normalized = normalizeServerConfig(name, config);
100
+ if (normalized) {
101
+ result[name] = normalized;
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function mergeServers(
108
+ base: Record<string, ServerConfig>,
109
+ overrides: Record<string, Partial<ServerConfig>>,
110
+ ): Record<string, ServerConfig> {
111
+ const merged: Record<string, ServerConfig> = { ...base };
112
+ for (const [name, config] of Object.entries(overrides)) {
113
+ if (merged[name]) {
114
+ const candidate = { ...merged[name], ...config };
115
+ const normalized = normalizeServerConfig(name, candidate);
116
+ if (normalized) {
117
+ merged[name] = normalized;
118
+ }
119
+ } else {
120
+ const normalized = normalizeServerConfig(name, config);
121
+ if (normalized) {
122
+ merged[name] = normalized;
123
+ }
124
+ }
125
+ }
126
+ return merged;
127
+ }
128
+
129
+ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
130
+ const updated: Record<string, ServerConfig> = { ...servers };
131
+
132
+ if (updated.omnisharp?.args) {
133
+ const args = updated.omnisharp.args.map((arg: string) => (arg === PID_TOKEN ? String(process.pid) : arg));
134
+ updated.omnisharp = { ...updated.omnisharp, args };
135
+ }
136
+
137
+ return updated;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Configuration Loading
142
+ // =============================================================================
143
+
144
+ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
145
+ for (const marker of markers) {
146
+ if (marker.includes("*")) {
147
+ try {
148
+ const matches = globSync(marker, { cwd, nodir: false });
149
+ if (matches.length > 0) {
150
+ return true;
151
+ }
152
+ } catch {
153
+ // Failed to resolve glob root marker
154
+ }
155
+ continue;
156
+ }
157
+ const filePath = path.join(cwd, marker);
158
+ if (fs.existsSync(filePath)) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ // =============================================================================
166
+ // Local Binary Resolution
167
+ // =============================================================================
168
+
169
+ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
170
+ { markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
171
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" },
172
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" },
173
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" },
174
+ { markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
175
+ { markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
176
+ { markers: ["go.mod", "go.sum"], binDir: "bin" },
177
+ ];
178
+
179
+ function which(command: string): string | null {
180
+ const result = spawnSync("which", [command], { encoding: "utf-8" });
181
+ if (result.status !== 0) return null;
182
+ return result.stdout.trim() || null;
183
+ }
184
+
185
+ export function resolveCommand(command: string, cwd: string): string | null {
186
+ for (const { markers, binDir } of LOCAL_BIN_PATHS) {
187
+ if (hasRootMarkers(cwd, markers)) {
188
+ const localPath = path.join(cwd, binDir, command);
189
+ if (fs.existsSync(localPath)) {
190
+ return localPath;
191
+ }
192
+ }
193
+ }
194
+
195
+ return which(command);
196
+ }
197
+
198
+ /**
199
+ * Configuration file search paths (in priority order).
200
+ */
201
+ function getConfigPaths(cwd: string): string[] {
202
+ const filenames = ["lsp.json", ".lsp.json", "lsp.yaml", ".lsp.yaml", "lsp.yml", ".lsp.yml"];
203
+ const paths: string[] = [];
204
+
205
+ // Project root files (highest priority)
206
+ for (const filename of filenames) {
207
+ paths.push(path.join(cwd, filename));
208
+ }
209
+
210
+ // Project config directory
211
+ const projectConfigDir = path.join(cwd, CONFIG_DIR_NAME);
212
+ for (const filename of filenames) {
213
+ paths.push(path.join(projectConfigDir, filename));
214
+ }
215
+
216
+ // User config directory
217
+ const userConfigDir = path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
218
+ for (const filename of filenames) {
219
+ paths.push(path.join(userConfigDir, filename));
220
+ }
221
+
222
+ // User home root files (lowest priority fallback)
223
+ for (const filename of filenames) {
224
+ paths.push(path.join(os.homedir(), filename));
225
+ }
226
+
227
+ return paths;
228
+ }
229
+
230
+ /**
231
+ * Load LSP configuration.
232
+ *
233
+ * Priority (highest to lowest):
234
+ * 1. Project root: lsp.json/.lsp.json/lsp.yml/.lsp.yml/lsp.yaml/.lsp.yaml
235
+ * 2. Project config dir: {CONFIG_DIR_NAME}/lsp.* (+ hidden variants)
236
+ * 3. User config dir: ~/{CONFIG_DIR_NAME}/agent/lsp.* (+ hidden variants)
237
+ * 4. User home root: ~/lsp.*, ~/.lsp.*
238
+ * 5. Auto-detect from project markers + available binaries
239
+ */
240
+ export function loadConfig(cwd: string): LspConfig {
241
+ let mergedServers = coerceServerConfigs(DEFAULTS);
242
+
243
+ const configPaths = getConfigPaths(cwd).reverse();
244
+ let hasOverrides = false;
245
+
246
+ let idleTimeoutMs: number | undefined;
247
+ for (const configPath of configPaths) {
248
+ const parsed = readConfigFile(configPath);
249
+ if (!parsed) continue;
250
+ const hasServerOverrides = Object.keys(parsed.servers).length > 0;
251
+ if (hasServerOverrides) {
252
+ hasOverrides = true;
253
+ mergedServers = mergeServers(mergedServers, parsed.servers);
254
+ }
255
+ if (parsed.idleTimeoutMs !== undefined) {
256
+ idleTimeoutMs = parsed.idleTimeoutMs;
257
+ }
258
+ }
259
+
260
+ if (!hasOverrides) {
261
+ const detected: Record<string, ServerConfig> = {};
262
+ const defaultsWithRuntime = applyRuntimeDefaults(mergedServers);
263
+
264
+ for (const [name, config] of Object.entries(defaultsWithRuntime)) {
265
+ if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
266
+ const resolved = resolveCommand(config.command, cwd);
267
+ if (!resolved) continue;
268
+ detected[name] = { ...config, resolvedCommand: resolved };
269
+ }
270
+
271
+ return { servers: detected, idleTimeoutMs };
272
+ }
273
+
274
+ const mergedWithRuntime = applyRuntimeDefaults(mergedServers);
275
+ const available: Record<string, ServerConfig> = {};
276
+
277
+ for (const [name, config] of Object.entries(mergedWithRuntime)) {
278
+ if (config.disabled) continue;
279
+ const resolved = resolveCommand(config.command, cwd);
280
+ if (!resolved) continue;
281
+ available[name] = { ...config, resolvedCommand: resolved };
282
+ }
283
+
284
+ return { servers: available, idleTimeoutMs };
285
+ }
286
+
287
+ // =============================================================================
288
+ // Server Selection
289
+ // =============================================================================
290
+
291
+ export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
292
+ const ext = path.extname(filePath).toLowerCase();
293
+ const fileName = path.basename(filePath).toLowerCase();
294
+ const matches: Array<[string, ServerConfig]> = [];
295
+
296
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
297
+ const supportsFile = serverConfig.fileTypes.some(fileType => {
298
+ const normalized = fileType.toLowerCase();
299
+ return normalized === ext || normalized === fileName;
300
+ });
301
+
302
+ if (supportsFile) {
303
+ matches.push([name, serverConfig]);
304
+ }
305
+ }
306
+
307
+ // Sort: primary servers (non-linters) first, then linters
308
+ return matches.sort((a, b) => {
309
+ const aIsLinter = a[1].isLinter ? 1 : 0;
310
+ const bIsLinter = b[1].isLinter ? 1 : 0;
311
+ return aIsLinter - bIsLinter;
312
+ });
313
+ }
314
+
315
+ export function getServerForFile(config: LspConfig, filePath: string): [string, ServerConfig] | null {
316
+ const servers = getServersForFile(config, filePath);
317
+ return servers.length > 0 ? servers[0] : null;
318
+ }
319
+
320
+ export function hasCapability(
321
+ config: ServerConfig,
322
+ capability: keyof NonNullable<ServerConfig["capabilities"]>,
323
+ ): boolean {
324
+ return config.capabilities?.[capability] === true;
325
+ }