@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 (157) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/complete.ts +2 -4
  16. package/src/core/tools/edit-diff.ts +11 -4
  17. package/src/core/tools/edit.ts +7 -13
  18. package/src/core/tools/find.ts +111 -50
  19. package/src/core/tools/gemini-image.ts +128 -147
  20. package/src/core/tools/grep.ts +397 -415
  21. package/src/core/tools/index.test.ts +5 -1
  22. package/src/core/tools/index.ts +6 -8
  23. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +58 -9
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +55 -32
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +152 -76
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/executor.ts +204 -67
  37. package/src/core/tools/task/index.ts +129 -92
  38. package/src/core/tools/task/name-generator.ts +1544 -214
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +34 -11
  42. package/src/core/tools/task/worker.ts +152 -27
  43. package/src/core/tools/web-fetch.ts +220 -1657
  44. package/src/core/tools/web-scrapers/academic.test.ts +239 -0
  45. package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
  46. package/src/core/tools/web-scrapers/arxiv.ts +88 -0
  47. package/src/core/tools/web-scrapers/aur.ts +175 -0
  48. package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
  49. package/src/core/tools/web-scrapers/bluesky.ts +284 -0
  50. package/src/core/tools/web-scrapers/brew.ts +177 -0
  51. package/src/core/tools/web-scrapers/business.test.ts +82 -0
  52. package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
  53. package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
  54. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  55. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  56. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  57. package/src/core/tools/web-scrapers/coingecko.ts +184 -0
  58. package/src/core/tools/web-scrapers/crates-io.ts +128 -0
  59. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  60. package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
  61. package/src/core/tools/web-scrapers/devto.ts +177 -0
  62. package/src/core/tools/web-scrapers/discogs.ts +308 -0
  63. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  64. package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
  65. package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
  66. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  67. package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
  68. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  69. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  70. package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
  71. package/src/core/tools/web-scrapers/github-gist.ts +68 -0
  72. package/src/core/tools/web-scrapers/github.ts +455 -0
  73. package/src/core/tools/web-scrapers/gitlab.ts +456 -0
  74. package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
  75. package/src/core/tools/web-scrapers/hackage.ts +94 -0
  76. package/src/core/tools/web-scrapers/hackernews.ts +208 -0
  77. package/src/core/tools/web-scrapers/hex.ts +121 -0
  78. package/src/core/tools/web-scrapers/huggingface.ts +385 -0
  79. package/src/core/tools/web-scrapers/iacr.ts +86 -0
  80. package/src/core/tools/web-scrapers/index.ts +250 -0
  81. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  82. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  83. package/src/core/tools/web-scrapers/lobsters.ts +186 -0
  84. package/src/core/tools/web-scrapers/mastodon.ts +310 -0
  85. package/src/core/tools/web-scrapers/maven.ts +152 -0
  86. package/src/core/tools/web-scrapers/mdn.ts +174 -0
  87. package/src/core/tools/web-scrapers/media.test.ts +138 -0
  88. package/src/core/tools/web-scrapers/metacpan.ts +253 -0
  89. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  90. package/src/core/tools/web-scrapers/npm.ts +114 -0
  91. package/src/core/tools/web-scrapers/nuget.ts +205 -0
  92. package/src/core/tools/web-scrapers/nvd.ts +243 -0
  93. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  94. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  95. package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
  96. package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
  97. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  98. package/src/core/tools/web-scrapers/osv.ts +189 -0
  99. package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
  100. package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
  101. package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
  102. package/src/core/tools/web-scrapers/packagist.ts +174 -0
  103. package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
  104. package/src/core/tools/web-scrapers/pubmed.ts +178 -0
  105. package/src/core/tools/web-scrapers/pypi.ts +129 -0
  106. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  107. package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
  108. package/src/core/tools/web-scrapers/reddit.ts +104 -0
  109. package/src/core/tools/web-scrapers/repology.ts +262 -0
  110. package/src/core/tools/web-scrapers/research.test.ts +107 -0
  111. package/src/core/tools/web-scrapers/rfc.ts +209 -0
  112. package/src/core/tools/web-scrapers/rubygems.ts +117 -0
  113. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  114. package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
  115. package/src/core/tools/web-scrapers/security.test.ts +103 -0
  116. package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
  117. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  118. package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
  119. package/src/core/tools/web-scrapers/social.test.ts +259 -0
  120. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  121. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  122. package/src/core/tools/web-scrapers/spotify.ts +218 -0
  123. package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
  124. package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
  125. package/src/core/tools/web-scrapers/standards.test.ts +122 -0
  126. package/src/core/tools/web-scrapers/terraform.ts +304 -0
  127. package/src/core/tools/web-scrapers/tldr.ts +51 -0
  128. package/src/core/tools/web-scrapers/twitter.ts +96 -0
  129. package/src/core/tools/web-scrapers/types.ts +234 -0
  130. package/src/core/tools/web-scrapers/utils.ts +162 -0
  131. package/src/core/tools/web-scrapers/vimeo.ts +152 -0
  132. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  133. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  134. package/src/core/tools/web-scrapers/wikidata.ts +357 -0
  135. package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
  136. package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
  137. package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
  138. package/src/core/tools/web-scrapers/youtube.ts +371 -0
  139. package/src/core/tools/write.ts +21 -18
  140. package/src/core/voice.ts +3 -2
  141. package/src/lib/worktree/collapse.ts +2 -1
  142. package/src/lib/worktree/git.ts +2 -18
  143. package/src/main.ts +59 -3
  144. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  145. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  146. package/src/modes/interactive/components/hook-editor.ts +2 -1
  147. package/src/modes/interactive/components/model-selector.ts +19 -4
  148. package/src/modes/interactive/interactive-mode.ts +41 -38
  149. package/src/modes/interactive/theme/theme.ts +58 -58
  150. package/src/modes/rpc/rpc-mode.ts +10 -9
  151. package/src/prompts/review-request.md +27 -0
  152. package/src/prompts/reviewer.md +64 -68
  153. package/src/prompts/tools/output.md +22 -3
  154. package/src/prompts/tools/task.md +32 -33
  155. package/src/utils/clipboard.ts +2 -1
  156. package/src/utils/tools-manager.ts +110 -8
  157. package/examples/extensions/subagent/agents/reviewer.md +0 -35
@@ -1,8 +1,11 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  import { homedir } from "node:os";
3
- import { extname, join } from "node:path";
2
+ import { basename, extname, join } from "node:path";
3
+ import { globSync } from "glob";
4
+ import { parse as parseYaml } from "yaml";
4
5
  import { getConfigDirPaths } from "../../../config";
6
+ import { logger } from "../../logger";
5
7
  import { createBiomeClient } from "./clients/biome-client";
8
+ import DEFAULTS from "./defaults.json" with { type: "json" };
6
9
  import type { ServerConfig } from "./types";
7
10
 
8
11
  export interface LspConfig {
@@ -12,599 +15,138 @@ export interface LspConfig {
12
15
  }
13
16
 
14
17
  // =============================================================================
15
- // Predefined Server Configurations
18
+ // Default Server Configuration Loading
16
19
  // =============================================================================
17
20
 
18
- /**
19
- * Comprehensive LSP server configurations.
20
- *
21
- * Each server can be customized via lsp.json config file with these options:
22
- * - command: Binary name or path
23
- * - args: Command line arguments
24
- * - fileTypes: File extensions this server handles
25
- * - rootMarkers: Files that indicate project root
26
- * - initOptions: LSP initialization options
27
- * - settings: LSP workspace settings
28
- * - disabled: Set to true to disable this server
29
- * - isLinter: If true, used only for diagnostics/actions (not type intelligence)
30
- */
31
- export const SERVERS: Record<string, ServerConfig> = {
32
- // =========================================================================
33
- // Systems Languages
34
- // =========================================================================
35
-
36
- "rust-analyzer": {
37
- command: "rust-analyzer",
38
- args: [],
39
- fileTypes: [".rs"],
40
- rootMarkers: ["Cargo.toml", "rust-analyzer.toml"],
41
- initOptions: {
42
- checkOnSave: { command: "clippy" },
43
- cargo: { allFeatures: true },
44
- procMacro: { enable: true },
45
- },
46
- settings: {
47
- "rust-analyzer": {
48
- diagnostics: { enable: true },
49
- inlayHints: { enable: true },
50
- },
51
- },
52
- capabilities: {
53
- flycheck: true,
54
- ssr: true,
55
- expandMacro: true,
56
- runnables: true,
57
- relatedTests: true,
58
- },
59
- },
60
-
61
- clangd: {
62
- command: "clangd",
63
- args: ["--background-index", "--clang-tidy", "--header-insertion=iwyu"],
64
- fileTypes: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx", ".m", ".mm"],
65
- rootMarkers: ["compile_commands.json", "CMakeLists.txt", ".clangd", ".clang-format", "Makefile"],
66
- },
67
-
68
- zls: {
69
- command: "zls",
70
- args: [],
71
- fileTypes: [".zig"],
72
- rootMarkers: ["build.zig", "build.zig.zon", "zls.json"],
73
- },
74
-
75
- gopls: {
76
- command: "gopls",
77
- args: ["serve"],
78
- fileTypes: [".go", ".mod", ".sum"],
79
- rootMarkers: ["go.mod", "go.work", "go.sum"],
80
- settings: {
81
- gopls: {
82
- analyses: { unusedparams: true, shadow: true },
83
- staticcheck: true,
84
- gofumpt: true,
85
- },
86
- },
87
- },
88
-
89
- // =========================================================================
90
- // JavaScript/TypeScript Ecosystem
91
- // =========================================================================
92
-
93
- "typescript-language-server": {
94
- command: "typescript-language-server",
95
- args: ["--stdio"],
96
- fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
97
- rootMarkers: ["package.json", "tsconfig.json", "jsconfig.json"],
98
- initOptions: {
99
- hostInfo: "omp-coding-agent",
100
- preferences: {
101
- includeInlayParameterNameHints: "all",
102
- includeInlayVariableTypeHints: true,
103
- includeInlayFunctionParameterTypeHints: true,
104
- },
105
- },
106
- },
107
-
108
- biome: {
109
- command: "biome",
110
- args: ["lsp-proxy"],
111
- fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc"],
112
- rootMarkers: ["biome.json", "biome.jsonc"],
113
- isLinter: true,
114
- // Use CLI instead of LSP - Biome's LSP has known stale diagnostics issues
115
- createClient: createBiomeClient,
116
- },
117
-
118
- eslint: {
119
- command: "vscode-eslint-language-server",
120
- args: ["--stdio"],
121
- fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
122
- rootMarkers: [
123
- ".eslintrc",
124
- ".eslintrc.js",
125
- ".eslintrc.json",
126
- ".eslintrc.yml",
127
- "eslint.config.js",
128
- "eslint.config.mjs",
129
- ],
130
- isLinter: true,
131
- settings: {
132
- validate: "on",
133
- run: "onType",
134
- },
135
- },
136
-
137
- denols: {
138
- command: "deno",
139
- args: ["lsp"],
140
- fileTypes: [".ts", ".tsx", ".js", ".jsx"],
141
- rootMarkers: ["deno.json", "deno.jsonc", "deno.lock"],
142
- initOptions: {
143
- enable: true,
144
- lint: true,
145
- unstable: true,
146
- },
147
- },
148
-
149
- // =========================================================================
150
- // Web Technologies
151
- // =========================================================================
152
-
153
- "vscode-html-language-server": {
154
- command: "vscode-html-language-server",
155
- args: ["--stdio"],
156
- fileTypes: [".html", ".htm"],
157
- rootMarkers: ["package.json", ".git"],
158
- initOptions: {
159
- provideFormatter: true,
160
- },
161
- },
162
-
163
- "vscode-css-language-server": {
164
- command: "vscode-css-language-server",
165
- args: ["--stdio"],
166
- fileTypes: [".css", ".scss", ".sass", ".less"],
167
- rootMarkers: ["package.json", ".git"],
168
- initOptions: {
169
- provideFormatter: true,
170
- },
171
- },
172
-
173
- "vscode-json-language-server": {
174
- command: "vscode-json-language-server",
175
- args: ["--stdio"],
176
- fileTypes: [".json", ".jsonc"],
177
- rootMarkers: ["package.json", ".git"],
178
- initOptions: {
179
- provideFormatter: true,
180
- },
181
- },
182
-
183
- tailwindcss: {
184
- command: "tailwindcss-language-server",
185
- args: ["--stdio"],
186
- fileTypes: [".html", ".css", ".scss", ".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte"],
187
- rootMarkers: ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"],
188
- },
189
-
190
- svelte: {
191
- command: "svelteserver",
192
- args: ["--stdio"],
193
- fileTypes: [".svelte"],
194
- rootMarkers: ["svelte.config.js", "svelte.config.mjs", "package.json"],
195
- },
196
-
197
- "vue-language-server": {
198
- command: "vue-language-server",
199
- args: ["--stdio"],
200
- fileTypes: [".vue"],
201
- rootMarkers: ["vue.config.js", "nuxt.config.js", "nuxt.config.ts", "package.json"],
202
- },
203
-
204
- astro: {
205
- command: "astro-ls",
206
- args: ["--stdio"],
207
- fileTypes: [".astro"],
208
- rootMarkers: ["astro.config.mjs", "astro.config.js", "astro.config.ts"],
209
- },
210
-
211
- // =========================================================================
212
- // Python
213
- // =========================================================================
214
-
215
- pyright: {
216
- command: "pyright-langserver",
217
- args: ["--stdio"],
218
- fileTypes: [".py", ".pyi"],
219
- rootMarkers: ["pyproject.toml", "pyrightconfig.json", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"],
220
- settings: {
221
- python: {
222
- analysis: {
223
- autoSearchPaths: true,
224
- diagnosticMode: "openFilesOnly",
225
- useLibraryCodeForTypes: true,
226
- },
227
- },
228
- },
229
- },
230
-
231
- basedpyright: {
232
- command: "basedpyright-langserver",
233
- args: ["--stdio"],
234
- fileTypes: [".py", ".pyi"],
235
- rootMarkers: ["pyproject.toml", "pyrightconfig.json", "setup.py", "requirements.txt"],
236
- settings: {
237
- basedpyright: {
238
- analysis: {
239
- autoSearchPaths: true,
240
- diagnosticMode: "openFilesOnly",
241
- useLibraryCodeForTypes: true,
242
- },
243
- },
244
- },
245
- },
246
-
247
- pylsp: {
248
- command: "pylsp",
249
- args: [],
250
- fileTypes: [".py"],
251
- rootMarkers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"],
252
- },
253
-
254
- ruff: {
255
- command: "ruff",
256
- args: ["server"],
257
- fileTypes: [".py", ".pyi"],
258
- rootMarkers: ["pyproject.toml", "ruff.toml", ".ruff.toml"],
259
- isLinter: true,
260
- },
261
-
262
- // =========================================================================
263
- // JVM Languages
264
- // =========================================================================
265
-
266
- jdtls: {
267
- command: "jdtls",
268
- args: [],
269
- fileTypes: [".java"],
270
- rootMarkers: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", ".project"],
271
- },
272
-
273
- "kotlin-language-server": {
274
- command: "kotlin-language-server",
275
- args: [],
276
- fileTypes: [".kt", ".kts"],
277
- rootMarkers: ["build.gradle", "build.gradle.kts", "pom.xml", "settings.gradle", "settings.gradle.kts"],
278
- },
279
-
280
- metals: {
281
- command: "metals",
282
- args: [],
283
- fileTypes: [".scala", ".sbt", ".sc"],
284
- rootMarkers: ["build.sbt", "build.sc", "build.gradle", "pom.xml"],
285
- initOptions: {
286
- statusBarProvider: "show-message",
287
- isHttpEnabled: true,
288
- },
289
- },
290
-
291
- // =========================================================================
292
- // Functional Languages
293
- // =========================================================================
294
-
295
- hls: {
296
- command: "haskell-language-server-wrapper",
297
- args: ["--lsp"],
298
- fileTypes: [".hs", ".lhs"],
299
- rootMarkers: ["stack.yaml", "cabal.project", "hie.yaml", "package.yaml", "*.cabal"],
300
- settings: {
301
- haskell: {
302
- formattingProvider: "ormolu",
303
- checkProject: true,
304
- },
305
- },
306
- },
307
-
308
- ocamllsp: {
309
- command: "ocamllsp",
310
- args: [],
311
- fileTypes: [".ml", ".mli", ".mll", ".mly"],
312
- rootMarkers: ["dune-project", "dune-workspace", "*.opam", ".ocamlformat"],
313
- },
314
-
315
- elixirls: {
316
- command: "elixir-ls",
317
- args: [],
318
- fileTypes: [".ex", ".exs", ".heex", ".eex"],
319
- rootMarkers: ["mix.exs", "mix.lock"],
320
- settings: {
321
- elixirLS: {
322
- dialyzerEnabled: true,
323
- fetchDeps: false,
324
- },
325
- },
326
- },
327
-
328
- erlangls: {
329
- command: "erlang_ls",
330
- args: [],
331
- fileTypes: [".erl", ".hrl"],
332
- rootMarkers: ["rebar.config", "erlang.mk", "rebar.lock"],
333
- },
334
-
335
- gleam: {
336
- command: "gleam",
337
- args: ["lsp"],
338
- fileTypes: [".gleam"],
339
- rootMarkers: ["gleam.toml"],
340
- },
341
-
342
- // =========================================================================
343
- // Ruby
344
- // =========================================================================
345
-
346
- solargraph: {
347
- command: "solargraph",
348
- args: ["stdio"],
349
- fileTypes: [".rb", ".rake", ".gemspec"],
350
- rootMarkers: ["Gemfile", ".solargraph.yml", "Rakefile"],
351
- initOptions: {
352
- formatting: true,
353
- },
354
- settings: {
355
- solargraph: {
356
- diagnostics: true,
357
- completion: true,
358
- hover: true,
359
- formatting: true,
360
- references: true,
361
- rename: true,
362
- symbols: true,
363
- },
364
- },
365
- },
366
-
367
- "ruby-lsp": {
368
- command: "ruby-lsp",
369
- args: [],
370
- fileTypes: [".rb", ".rake", ".gemspec", ".erb"],
371
- rootMarkers: ["Gemfile", ".ruby-version", ".ruby-gemset"],
372
- initOptions: {
373
- formatter: "auto",
374
- },
375
- },
376
-
377
- rubocop: {
378
- command: "rubocop",
379
- args: ["--lsp"],
380
- fileTypes: [".rb", ".rake"],
381
- rootMarkers: [".rubocop.yml", "Gemfile"],
382
- isLinter: true,
383
- },
384
-
385
- // =========================================================================
386
- // Shell / Scripting
387
- // =========================================================================
388
-
389
- bashls: {
390
- command: "bash-language-server",
391
- args: ["start"],
392
- fileTypes: [".sh", ".bash", ".zsh"],
393
- rootMarkers: [".git"],
394
- settings: {
395
- bashIde: {
396
- globPattern: "*@(.sh|.inc|.bash|.command)",
397
- },
398
- },
399
- },
400
-
401
- nushell: {
402
- command: "nu",
403
- args: ["--lsp"],
404
- fileTypes: [".nu"],
405
- rootMarkers: [".git"],
406
- },
407
-
408
- // =========================================================================
409
- // Lua
410
- // =========================================================================
411
-
412
- "lua-language-server": {
413
- command: "lua-language-server",
414
- args: [],
415
- fileTypes: [".lua"],
416
- rootMarkers: [".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml"],
417
- settings: {
418
- Lua: {
419
- runtime: { version: "LuaJIT" },
420
- diagnostics: { globals: ["vim"] },
421
- workspace: { checkThirdParty: false },
422
- telemetry: { enable: false },
423
- },
424
- },
425
- },
426
-
427
- // =========================================================================
428
- // PHP
429
- // =========================================================================
430
-
431
- intelephense: {
432
- command: "intelephense",
433
- args: ["--stdio"],
434
- fileTypes: [".php", ".phtml"],
435
- rootMarkers: ["composer.json", "composer.lock", ".git"],
436
- },
437
-
438
- phpactor: {
439
- command: "phpactor",
440
- args: ["language-server"],
441
- fileTypes: [".php"],
442
- rootMarkers: ["composer.json", ".phpactor.json", ".phpactor.yml"],
443
- },
444
-
445
- // =========================================================================
446
- // .NET
447
- // =========================================================================
448
-
449
- omnisharp: {
450
- command: "omnisharp",
451
- args: ["-z", "--hostPID", String(process.pid), "--encoding", "utf-8", "--languageserver"],
452
- fileTypes: [".cs", ".csx"],
453
- rootMarkers: ["*.sln", "*.csproj", "omnisharp.json", ".git"],
454
- settings: {
455
- FormattingOptions: { EnableEditorConfigSupport: true },
456
- RoslynExtensionsOptions: { EnableAnalyzersSupport: true },
457
- },
458
- },
459
-
460
- // =========================================================================
461
- // Configuration Languages
462
- // =========================================================================
463
-
464
- yamlls: {
465
- command: "yaml-language-server",
466
- args: ["--stdio"],
467
- fileTypes: [".yaml", ".yml"],
468
- rootMarkers: [".git"],
469
- settings: {
470
- yaml: {
471
- validate: true,
472
- format: { enable: true },
473
- hover: true,
474
- completion: true,
475
- },
476
- redhat: { telemetry: { enabled: false } },
477
- },
478
- },
479
-
480
- taplo: {
481
- command: "taplo",
482
- args: ["lsp", "stdio"],
483
- fileTypes: [".toml"],
484
- rootMarkers: [".taplo.toml", "taplo.toml", ".git"],
485
- },
486
-
487
- terraformls: {
488
- command: "terraform-ls",
489
- args: ["serve"],
490
- fileTypes: [".tf", ".tfvars"],
491
- rootMarkers: [".terraform", "terraform.tfstate", "*.tf"],
492
- },
493
-
494
- dockerls: {
495
- command: "docker-langserver",
496
- args: ["--stdio"],
497
- fileTypes: [".dockerfile"],
498
- rootMarkers: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore"],
499
- },
500
-
501
- "helm-ls": {
502
- command: "helm_ls",
503
- args: ["serve"],
504
- fileTypes: [".yaml", ".yml", ".tpl"],
505
- rootMarkers: ["Chart.yaml", "Chart.yml"],
506
- },
507
-
508
- // =========================================================================
509
- // Nix
510
- // =========================================================================
511
-
512
- nixd: {
513
- command: "nixd",
514
- args: [],
515
- fileTypes: [".nix"],
516
- rootMarkers: ["flake.nix", "default.nix", "shell.nix"],
517
- },
518
-
519
- nil: {
520
- command: "nil",
521
- args: [],
522
- fileTypes: [".nix"],
523
- rootMarkers: ["flake.nix", "default.nix", "shell.nix"],
524
- },
525
-
526
- // =========================================================================
527
- // Other Languages
528
- // =========================================================================
529
-
530
- ols: {
531
- command: "ols",
532
- args: [],
533
- fileTypes: [".odin"],
534
- rootMarkers: ["ols.json", ".git"],
535
- },
536
-
537
- dartls: {
538
- command: "dart",
539
- args: ["language-server", "--protocol=lsp"],
540
- fileTypes: [".dart"],
541
- rootMarkers: ["pubspec.yaml", "pubspec.lock"],
542
- initOptions: {
543
- closingLabels: true,
544
- flutterOutline: true,
545
- outline: true,
546
- },
547
- },
548
-
549
- marksman: {
550
- command: "marksman",
551
- args: ["server"],
552
- fileTypes: [".md", ".markdown"],
553
- rootMarkers: [".marksman.toml", ".git"],
554
- },
555
-
556
- texlab: {
557
- command: "texlab",
558
- args: [],
559
- fileTypes: [".tex", ".bib", ".sty", ".cls"],
560
- rootMarkers: [".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot", "Tectonic.toml"],
561
- settings: {
562
- texlab: {
563
- build: {
564
- executable: "latexmk",
565
- args: ["-pdf", "-interaction=nonstopmode", "-synctex=1", "%f"],
566
- },
567
- chktex: { onOpenAndSave: true },
568
- },
569
- },
570
- },
571
-
572
- graphql: {
573
- command: "graphql-lsp",
574
- args: ["server", "-m", "stream"],
575
- fileTypes: [".graphql", ".gql"],
576
- rootMarkers: [".graphqlrc", ".graphqlrc.json", ".graphqlrc.yml", ".graphqlrc.yaml", "graphql.config.js"],
577
- },
578
-
579
- prismals: {
580
- command: "prisma-language-server",
581
- args: ["--stdio"],
582
- fileTypes: [".prisma"],
583
- rootMarkers: ["schema.prisma", "prisma/schema.prisma"],
584
- },
585
-
586
- vimls: {
587
- command: "vim-language-server",
588
- args: ["--stdio"],
589
- fileTypes: [".vim", ".vimrc"],
590
- rootMarkers: [".git"],
591
- initOptions: {
592
- isNeovim: true,
593
- diagnostic: { enable: true },
594
- },
595
- },
596
-
597
- // =========================================================================
598
- // Emmet (HTML/CSS expansion)
599
- // =========================================================================
600
-
601
- "emmet-language-server": {
602
- command: "emmet-language-server",
603
- args: ["--stdio"],
604
- fileTypes: [".html", ".css", ".scss", ".less", ".jsx", ".tsx", ".vue", ".svelte"],
605
- rootMarkers: [".git"],
606
- },
607
- };
21
+ const PID_TOKEN = "$PID";
22
+
23
+ interface NormalizedConfig {
24
+ servers: Record<string, Partial<ServerConfig>>;
25
+ idleTimeoutMs?: number;
26
+ }
27
+
28
+ function isRecord(value: unknown): value is Record<string, unknown> {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+
32
+ function parseConfigContent(content: string, filePath: string): unknown {
33
+ const extension = extname(filePath).toLowerCase();
34
+ if (extension === ".yaml" || extension === ".yml") {
35
+ return parseYaml(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
+ logger.warn("Ignoring invalid LSP server config (missing required fields).", { name });
71
+ return null;
72
+ }
73
+
74
+ const args = Array.isArray(config.args)
75
+ ? config.args.filter((entry): entry is string => typeof entry === "string")
76
+ : undefined;
77
+
78
+ return {
79
+ ...config,
80
+ command,
81
+ args,
82
+ fileTypes,
83
+ rootMarkers,
84
+ };
85
+ }
86
+
87
+ async function readConfigFile(filePath: string): Promise<NormalizedConfig | null> {
88
+ try {
89
+ const file = Bun.file(filePath);
90
+ if (!(await file.exists())) {
91
+ return null;
92
+ }
93
+ const content = await file.text();
94
+ const parsed = parseConfigContent(content, filePath);
95
+ return normalizeConfig(parsed);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function coerceServerConfigs(servers: Record<string, Partial<ServerConfig>>): Record<string, ServerConfig> {
102
+ const result: Record<string, ServerConfig> = {};
103
+ for (const [name, config] of Object.entries(servers)) {
104
+ const normalized = normalizeServerConfig(name, config);
105
+ if (normalized) {
106
+ result[name] = normalized;
107
+ }
108
+ }
109
+ return result;
110
+ }
111
+
112
+ function mergeServers(
113
+ base: Record<string, ServerConfig>,
114
+ overrides: Record<string, Partial<ServerConfig>>,
115
+ ): Record<string, ServerConfig> {
116
+ const merged: Record<string, ServerConfig> = { ...base };
117
+ for (const [name, config] of Object.entries(overrides)) {
118
+ if (merged[name]) {
119
+ const candidate = { ...merged[name], ...config };
120
+ const normalized = normalizeServerConfig(name, candidate);
121
+ if (normalized) {
122
+ merged[name] = normalized;
123
+ } else {
124
+ logger.warn("Ignoring invalid LSP overrides (keeping previous config).", { name });
125
+ }
126
+ } else {
127
+ const normalized = normalizeServerConfig(name, config);
128
+ if (normalized) {
129
+ merged[name] = normalized;
130
+ }
131
+ }
132
+ }
133
+ return merged;
134
+ }
135
+
136
+ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
137
+ const updated: Record<string, ServerConfig> = { ...servers };
138
+
139
+ if (updated.biome) {
140
+ updated.biome = { ...updated.biome, createClient: createBiomeClient };
141
+ }
142
+
143
+ if (updated.omnisharp?.args) {
144
+ const args = updated.omnisharp.args.map((arg) => (arg === PID_TOKEN ? String(process.pid) : arg));
145
+ updated.omnisharp = { ...updated.omnisharp, args };
146
+ }
147
+
148
+ return updated;
149
+ }
608
150
 
609
151
  // =============================================================================
610
152
  // Configuration Loading
@@ -613,22 +155,26 @@ export const SERVERS: Record<string, ServerConfig> = {
613
155
  /**
614
156
  * Check if any root marker file exists in the directory
615
157
  */
616
- export function hasRootMarkers(cwd: string, markers: string[]): boolean {
617
- return markers.some((marker) => {
158
+ export async function hasRootMarkers(cwd: string, markers: string[]): Promise<boolean> {
159
+ for (const marker of markers) {
618
160
  // Handle glob-like patterns (e.g., "*.cabal")
619
161
  if (marker.includes("*")) {
620
162
  try {
621
- const { globSync } = require("glob");
622
163
  const matches = globSync(join(cwd, marker));
623
- return matches.length > 0;
164
+ if (matches.length > 0) {
165
+ return true;
166
+ }
624
167
  } catch {
625
- // globSync not available, skip glob patterns
626
- return false;
168
+ logger.warn("Failed to resolve glob root marker.", { marker, cwd });
627
169
  }
170
+ continue;
628
171
  }
629
172
  const filePath = join(cwd, marker);
630
- return existsSync(filePath);
631
- });
173
+ if (await Bun.file(filePath).exists()) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
632
178
  }
633
179
 
634
180
  // =============================================================================
@@ -661,12 +207,12 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
661
207
  * @param cwd - Working directory to search from
662
208
  * @returns Absolute path to the executable, or null if not found
663
209
  */
664
- export function resolveCommand(command: string, cwd: string): string | null {
210
+ export async function resolveCommand(command: string, cwd: string): Promise<string | null> {
665
211
  // Check local bin directories based on project markers
666
212
  for (const { markers, binDir } of LOCAL_BIN_PATHS) {
667
- if (hasRootMarkers(cwd, markers)) {
213
+ if (await hasRootMarkers(cwd, markers)) {
668
214
  const localPath = join(cwd, binDir, command);
669
- if (existsSync(localPath)) {
215
+ if (await Bun.file(localPath).exists()) {
670
216
  return localPath;
671
217
  }
672
218
  }
@@ -681,7 +227,7 @@ export function resolveCommand(command: string, cwd: string): string | null {
681
227
  * Supports both visible and hidden variants at each config location.
682
228
  */
683
229
  function getConfigPaths(cwd: string): string[] {
684
- const filenames = ["lsp.json", ".lsp.json"];
230
+ const filenames = ["lsp.json", ".lsp.json", "lsp.yaml", ".lsp.yaml", "lsp.yml", ".lsp.yml"];
685
231
  const paths: string[] = [];
686
232
 
687
233
  // Project root files (highest priority)
@@ -716,14 +262,16 @@ function getConfigPaths(cwd: string): string[] {
716
262
  /**
717
263
  * Load LSP configuration.
718
264
  *
719
- * Priority:
720
- * 1. Project root: lsp.json, .lsp.json
721
- * 2. Project config dirs: .omp/lsp.json, .pi/lsp.json, .claude/lsp.json (+ hidden variants)
722
- * 3. User config dirs: ~/.omp/agent/lsp.json, ~/.pi/agent/lsp.json, ~/.claude/lsp.json (+ hidden variants)
723
- * 4. User home root: ~/lsp.json, ~/.lsp.json
265
+ * Priority (highest to lowest):
266
+ * 1. Project root: lsp.json/.lsp.json/lsp.yml/.lsp.yml/lsp.yaml/.lsp.yaml
267
+ * 2. Project config dirs: .omp/lsp.*, .pi/lsp.*, .claude/lsp.* (+ hidden variants)
268
+ * 3. User config dirs: ~/.omp/agent/lsp.*, ~/.pi/agent/lsp.*, ~/.claude/lsp.* (+ hidden variants)
269
+ * 4. User home root: ~/lsp.*, ~/.lsp.*
724
270
  * 5. Auto-detect from project markers + available binaries
725
271
  *
726
- * Config file format:
272
+ * Config files are merged from lowest to highest priority; later files override earlier settings.
273
+ *
274
+ * Config file format (JSON or YAML):
727
275
  * ```json
728
276
  * {
729
277
  * "servers": {
@@ -743,61 +291,56 @@ function getConfigPaths(cwd: string): string[] {
743
291
  * ```
744
292
  */
745
293
  export async function loadConfig(cwd: string): Promise<LspConfig> {
746
- const configPaths = getConfigPaths(cwd);
294
+ let mergedServers = coerceServerConfigs(DEFAULTS);
295
+
296
+ const configPaths = getConfigPaths(cwd).reverse();
297
+ let hasOverrides = false;
747
298
 
299
+ let idleTimeoutMs: number | undefined;
748
300
  for (const configPath of configPaths) {
749
- if (existsSync(configPath)) {
750
- try {
751
- const content = readFileSync(configPath, "utf-8");
752
- const parsed = JSON.parse(content);
753
-
754
- // Support both { servers: {...} } and direct server map
755
- const servers = parsed.servers || parsed;
756
-
757
- // Merge with defaults and filter to available
758
- const merged: Record<string, ServerConfig> = { ...SERVERS };
759
-
760
- for (const [name, config] of Object.entries(servers) as [string, Partial<ServerConfig>][]) {
761
- if (merged[name]) {
762
- // Merge with existing config
763
- merged[name] = { ...merged[name], ...config };
764
- } else {
765
- // Add new server config
766
- merged[name] = config as ServerConfig;
767
- }
768
- }
301
+ const parsed = await readConfigFile(configPath);
302
+ if (!parsed) continue;
303
+ const hasServerOverrides = Object.keys(parsed.servers).length > 0;
304
+ if (hasServerOverrides) {
305
+ hasOverrides = true;
306
+ mergedServers = mergeServers(mergedServers, parsed.servers);
307
+ }
308
+ if (parsed.idleTimeoutMs !== undefined) {
309
+ idleTimeoutMs = parsed.idleTimeoutMs;
310
+ }
311
+ }
769
312
 
770
- // Filter to only enabled servers with available commands
771
- const available: Record<string, ServerConfig> = {};
772
- for (const [name, config] of Object.entries(merged)) {
773
- if (config.disabled) continue;
774
- const resolved = resolveCommand(config.command, cwd);
775
- if (!resolved) continue;
776
- available[name] = { ...config, resolvedCommand: resolved };
777
- }
313
+ if (!hasOverrides) {
314
+ // Auto-detect: find servers based on project markers AND available binaries
315
+ const detected: Record<string, ServerConfig> = {};
316
+ const defaultsWithRuntime = applyRuntimeDefaults(mergedServers);
778
317
 
779
- return { servers: available };
780
- } catch {
781
- // Ignore parse errors, continue to next config or auto-detect
782
- }
318
+ for (const [name, config] of Object.entries(defaultsWithRuntime)) {
319
+ // Check if project has root markers for this language
320
+ if (!(await hasRootMarkers(cwd, config.rootMarkers))) continue;
321
+
322
+ // Check if the language server binary is available (local or $PATH)
323
+ const resolved = await resolveCommand(config.command, cwd);
324
+ if (!resolved) continue;
325
+
326
+ detected[name] = { ...config, resolvedCommand: resolved };
783
327
  }
784
- }
785
328
 
786
- // Auto-detect: find servers based on project markers AND available binaries
787
- const detected: Record<string, ServerConfig> = {};
329
+ return { servers: detected, idleTimeoutMs };
330
+ }
788
331
 
789
- for (const [name, config] of Object.entries(SERVERS)) {
790
- // Check if project has root markers for this language
791
- if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
332
+ // Merge overrides with defaults and filter to available servers
333
+ const mergedWithRuntime = applyRuntimeDefaults(mergedServers);
334
+ const available: Record<string, ServerConfig> = {};
792
335
 
793
- // Check if the language server binary is available (local or $PATH)
794
- const resolved = resolveCommand(config.command, cwd);
336
+ for (const [name, config] of Object.entries(mergedWithRuntime)) {
337
+ if (config.disabled) continue;
338
+ const resolved = await resolveCommand(config.command, cwd);
795
339
  if (!resolved) continue;
796
-
797
- detected[name] = { ...config, resolvedCommand: resolved };
340
+ available[name] = { ...config, resolvedCommand: resolved };
798
341
  }
799
342
 
800
- return { servers: detected };
343
+ return { servers: available, idleTimeoutMs };
801
344
  }
802
345
 
803
346
  // =============================================================================
@@ -810,10 +353,16 @@ export async function loadConfig(cwd: string): Promise<LspConfig> {
810
353
  */
811
354
  export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
812
355
  const ext = extname(filePath).toLowerCase();
356
+ const fileName = basename(filePath).toLowerCase();
813
357
  const matches: Array<[string, ServerConfig]> = [];
814
358
 
815
359
  for (const [name, serverConfig] of Object.entries(config.servers)) {
816
- if (serverConfig.fileTypes.includes(ext)) {
360
+ const supportsFile = serverConfig.fileTypes.some((fileType) => {
361
+ const normalized = fileType.toLowerCase();
362
+ return normalized === ext || normalized === fileName;
363
+ });
364
+
365
+ if (supportsFile) {
817
366
  matches.push([name, serverConfig]);
818
367
  }
819
368
  }