@oh-my-pi/pi-coding-agent 1.340.0 → 1.341.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 (32) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/package.json +1 -1
  3. package/src/cli/args.ts +8 -0
  4. package/src/core/agent-session.ts +32 -14
  5. package/src/core/model-resolver.ts +101 -0
  6. package/src/core/sdk.ts +50 -17
  7. package/src/core/session-manager.ts +117 -14
  8. package/src/core/settings-manager.ts +90 -19
  9. package/src/core/title-generator.ts +94 -0
  10. package/src/core/tools/bash.ts +1 -2
  11. package/src/core/tools/edit-diff.ts +2 -2
  12. package/src/core/tools/edit.ts +43 -5
  13. package/src/core/tools/grep.ts +3 -2
  14. package/src/core/tools/index.ts +73 -13
  15. package/src/core/tools/lsp/client.ts +45 -20
  16. package/src/core/tools/lsp/config.ts +708 -34
  17. package/src/core/tools/lsp/index.ts +423 -23
  18. package/src/core/tools/lsp/types.ts +5 -0
  19. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  20. package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
  21. package/src/core/tools/task/model-resolver.ts +52 -3
  22. package/src/core/tools/write.ts +67 -4
  23. package/src/index.ts +5 -0
  24. package/src/main.ts +23 -2
  25. package/src/modes/interactive/components/model-selector.ts +96 -18
  26. package/src/modes/interactive/components/session-selector.ts +20 -7
  27. package/src/modes/interactive/components/settings-defs.ts +50 -2
  28. package/src/modes/interactive/components/settings-selector.ts +8 -11
  29. package/src/modes/interactive/components/tool-execution.ts +18 -0
  30. package/src/modes/interactive/components/tree-selector.ts +2 -2
  31. package/src/modes/interactive/components/welcome.ts +40 -3
  32. package/src/modes/interactive/interactive-mode.ts +86 -9
@@ -5,10 +5,32 @@ import type { ServerConfig } from "./types.js";
5
5
 
6
6
  export interface LspConfig {
7
7
  servers: Record<string, ServerConfig>;
8
+ /** Idle timeout in milliseconds. If set, LSP clients will be shutdown after this period of inactivity. Disabled by default. */
9
+ idleTimeoutMs?: number;
8
10
  }
9
11
 
10
- // Predefined server configurations with capabilities
12
+ // =============================================================================
13
+ // Predefined Server Configurations
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Comprehensive LSP server configurations.
18
+ *
19
+ * Each server can be customized via lsp.json config file with these options:
20
+ * - command: Binary name or path
21
+ * - args: Command line arguments
22
+ * - fileTypes: File extensions this server handles
23
+ * - rootMarkers: Files that indicate project root
24
+ * - initOptions: LSP initialization options
25
+ * - settings: LSP workspace settings
26
+ * - disabled: Set to true to disable this server
27
+ * - isLinter: If true, used only for diagnostics/actions (not type intelligence)
28
+ */
11
29
  export const SERVERS: Record<string, ServerConfig> = {
30
+ // =========================================================================
31
+ // Systems Languages
32
+ // =========================================================================
33
+
12
34
  "rust-analyzer": {
13
35
  command: "rust-analyzer",
14
36
  args: [],
@@ -19,6 +41,12 @@ export const SERVERS: Record<string, ServerConfig> = {
19
41
  cargo: { allFeatures: true },
20
42
  procMacro: { enable: true },
21
43
  },
44
+ settings: {
45
+ "rust-analyzer": {
46
+ diagnostics: { enable: true },
47
+ inlayHints: { enable: true },
48
+ },
49
+ },
22
50
  capabilities: {
23
51
  flycheck: true,
24
52
  ssr: true,
@@ -27,81 +55,705 @@ export const SERVERS: Record<string, ServerConfig> = {
27
55
  relatedTests: true,
28
56
  },
29
57
  },
58
+
59
+ clangd: {
60
+ command: "clangd",
61
+ args: ["--background-index", "--clang-tidy", "--header-insertion=iwyu"],
62
+ fileTypes: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx", ".m", ".mm"],
63
+ rootMarkers: ["compile_commands.json", "CMakeLists.txt", ".clangd", ".clang-format", "Makefile"],
64
+ },
65
+
66
+ zls: {
67
+ command: "zls",
68
+ args: [],
69
+ fileTypes: [".zig"],
70
+ rootMarkers: ["build.zig", "build.zig.zon", "zls.json"],
71
+ },
72
+
73
+ gopls: {
74
+ command: "gopls",
75
+ args: ["serve"],
76
+ fileTypes: [".go", ".mod", ".sum"],
77
+ rootMarkers: ["go.mod", "go.work", "go.sum"],
78
+ settings: {
79
+ gopls: {
80
+ analyses: { unusedparams: true, shadow: true },
81
+ staticcheck: true,
82
+ gofumpt: true,
83
+ },
84
+ },
85
+ },
86
+
87
+ // =========================================================================
88
+ // JavaScript/TypeScript Ecosystem
89
+ // =========================================================================
90
+
30
91
  "typescript-language-server": {
31
92
  command: "typescript-language-server",
32
93
  args: ["--stdio"],
33
- fileTypes: [".ts", ".tsx", ".js", ".jsx"],
94
+ fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
34
95
  rootMarkers: ["package.json", "tsconfig.json", "jsconfig.json"],
96
+ initOptions: {
97
+ hostInfo: "pi-coding-agent",
98
+ preferences: {
99
+ includeInlayParameterNameHints: "all",
100
+ includeInlayVariableTypeHints: true,
101
+ includeInlayFunctionParameterTypeHints: true,
102
+ },
103
+ },
35
104
  },
36
- gopls: {
37
- command: "gopls",
38
- args: ["serve"],
39
- fileTypes: [".go"],
40
- rootMarkers: ["go.mod", "go.work"],
105
+
106
+ biome: {
107
+ command: "biome",
108
+ args: ["lsp-proxy"],
109
+ fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc"],
110
+ rootMarkers: ["biome.json", "biome.jsonc"],
111
+ isLinter: true,
112
+ },
113
+
114
+ eslint: {
115
+ command: "vscode-eslint-language-server",
116
+ args: ["--stdio"],
117
+ fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
118
+ rootMarkers: [
119
+ ".eslintrc",
120
+ ".eslintrc.js",
121
+ ".eslintrc.json",
122
+ ".eslintrc.yml",
123
+ "eslint.config.js",
124
+ "eslint.config.mjs",
125
+ ],
126
+ isLinter: true,
127
+ settings: {
128
+ validate: "on",
129
+ run: "onType",
130
+ },
41
131
  },
132
+
133
+ denols: {
134
+ command: "deno",
135
+ args: ["lsp"],
136
+ fileTypes: [".ts", ".tsx", ".js", ".jsx"],
137
+ rootMarkers: ["deno.json", "deno.jsonc", "deno.lock"],
138
+ initOptions: {
139
+ enable: true,
140
+ lint: true,
141
+ unstable: true,
142
+ },
143
+ },
144
+
145
+ // =========================================================================
146
+ // Web Technologies
147
+ // =========================================================================
148
+
149
+ "vscode-html-language-server": {
150
+ command: "vscode-html-language-server",
151
+ args: ["--stdio"],
152
+ fileTypes: [".html", ".htm"],
153
+ rootMarkers: ["package.json", ".git"],
154
+ initOptions: {
155
+ provideFormatter: true,
156
+ },
157
+ },
158
+
159
+ "vscode-css-language-server": {
160
+ command: "vscode-css-language-server",
161
+ args: ["--stdio"],
162
+ fileTypes: [".css", ".scss", ".sass", ".less"],
163
+ rootMarkers: ["package.json", ".git"],
164
+ initOptions: {
165
+ provideFormatter: true,
166
+ },
167
+ },
168
+
169
+ "vscode-json-language-server": {
170
+ command: "vscode-json-language-server",
171
+ args: ["--stdio"],
172
+ fileTypes: [".json", ".jsonc"],
173
+ rootMarkers: ["package.json", ".git"],
174
+ initOptions: {
175
+ provideFormatter: true,
176
+ },
177
+ },
178
+
179
+ tailwindcss: {
180
+ command: "tailwindcss-language-server",
181
+ args: ["--stdio"],
182
+ fileTypes: [".html", ".css", ".scss", ".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte"],
183
+ rootMarkers: ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"],
184
+ },
185
+
186
+ svelte: {
187
+ command: "svelteserver",
188
+ args: ["--stdio"],
189
+ fileTypes: [".svelte"],
190
+ rootMarkers: ["svelte.config.js", "svelte.config.mjs", "package.json"],
191
+ },
192
+
193
+ "vue-language-server": {
194
+ command: "vue-language-server",
195
+ args: ["--stdio"],
196
+ fileTypes: [".vue"],
197
+ rootMarkers: ["vue.config.js", "nuxt.config.js", "nuxt.config.ts", "package.json"],
198
+ },
199
+
200
+ astro: {
201
+ command: "astro-ls",
202
+ args: ["--stdio"],
203
+ fileTypes: [".astro"],
204
+ rootMarkers: ["astro.config.mjs", "astro.config.js", "astro.config.ts"],
205
+ },
206
+
207
+ // =========================================================================
208
+ // Python
209
+ // =========================================================================
210
+
42
211
  pyright: {
43
212
  command: "pyright-langserver",
44
213
  args: ["--stdio"],
214
+ fileTypes: [".py", ".pyi"],
215
+ rootMarkers: ["pyproject.toml", "pyrightconfig.json", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"],
216
+ settings: {
217
+ python: {
218
+ analysis: {
219
+ autoSearchPaths: true,
220
+ diagnosticMode: "openFilesOnly",
221
+ useLibraryCodeForTypes: true,
222
+ },
223
+ },
224
+ },
225
+ },
226
+
227
+ basedpyright: {
228
+ command: "basedpyright-langserver",
229
+ args: ["--stdio"],
230
+ fileTypes: [".py", ".pyi"],
231
+ rootMarkers: ["pyproject.toml", "pyrightconfig.json", "setup.py", "requirements.txt"],
232
+ settings: {
233
+ basedpyright: {
234
+ analysis: {
235
+ autoSearchPaths: true,
236
+ diagnosticMode: "openFilesOnly",
237
+ useLibraryCodeForTypes: true,
238
+ },
239
+ },
240
+ },
241
+ },
242
+
243
+ pylsp: {
244
+ command: "pylsp",
245
+ args: [],
45
246
  fileTypes: [".py"],
46
- rootMarkers: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
247
+ rootMarkers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"],
47
248
  },
48
- zls: {
49
- command: "zls",
249
+
250
+ ruff: {
251
+ command: "ruff",
252
+ args: ["server"],
253
+ fileTypes: [".py", ".pyi"],
254
+ rootMarkers: ["pyproject.toml", "ruff.toml", ".ruff.toml"],
255
+ isLinter: true,
256
+ },
257
+
258
+ // =========================================================================
259
+ // JVM Languages
260
+ // =========================================================================
261
+
262
+ jdtls: {
263
+ command: "jdtls",
50
264
  args: [],
51
- fileTypes: [".zig"],
52
- rootMarkers: ["build.zig", "build.zig.zon", "zls.json"],
265
+ fileTypes: [".java"],
266
+ rootMarkers: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", ".project"],
53
267
  },
54
- clangd: {
55
- command: "clangd",
56
- args: ["--background-index"],
57
- fileTypes: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"],
58
- rootMarkers: ["compile_commands.json", "CMakeLists.txt", ".clangd"],
268
+
269
+ "kotlin-language-server": {
270
+ command: "kotlin-language-server",
271
+ args: [],
272
+ fileTypes: [".kt", ".kts"],
273
+ rootMarkers: ["build.gradle", "build.gradle.kts", "pom.xml", "settings.gradle", "settings.gradle.kts"],
274
+ },
275
+
276
+ metals: {
277
+ command: "metals",
278
+ args: [],
279
+ fileTypes: [".scala", ".sbt", ".sc"],
280
+ rootMarkers: ["build.sbt", "build.sc", "build.gradle", "pom.xml"],
281
+ initOptions: {
282
+ statusBarProvider: "show-message",
283
+ isHttpEnabled: true,
284
+ },
285
+ },
286
+
287
+ // =========================================================================
288
+ // Functional Languages
289
+ // =========================================================================
290
+
291
+ hls: {
292
+ command: "haskell-language-server-wrapper",
293
+ args: ["--lsp"],
294
+ fileTypes: [".hs", ".lhs"],
295
+ rootMarkers: ["stack.yaml", "cabal.project", "hie.yaml", "package.yaml", "*.cabal"],
296
+ settings: {
297
+ haskell: {
298
+ formattingProvider: "ormolu",
299
+ checkProject: true,
300
+ },
301
+ },
302
+ },
303
+
304
+ ocamllsp: {
305
+ command: "ocamllsp",
306
+ args: [],
307
+ fileTypes: [".ml", ".mli", ".mll", ".mly"],
308
+ rootMarkers: ["dune-project", "dune-workspace", "*.opam", ".ocamlformat"],
309
+ },
310
+
311
+ elixirls: {
312
+ command: "elixir-ls",
313
+ args: [],
314
+ fileTypes: [".ex", ".exs", ".heex", ".eex"],
315
+ rootMarkers: ["mix.exs", "mix.lock"],
316
+ settings: {
317
+ elixirLS: {
318
+ dialyzerEnabled: true,
319
+ fetchDeps: false,
320
+ },
321
+ },
322
+ },
323
+
324
+ erlangls: {
325
+ command: "erlang_ls",
326
+ args: [],
327
+ fileTypes: [".erl", ".hrl"],
328
+ rootMarkers: ["rebar.config", "erlang.mk", "rebar.lock"],
329
+ },
330
+
331
+ gleam: {
332
+ command: "gleam",
333
+ args: ["lsp"],
334
+ fileTypes: [".gleam"],
335
+ rootMarkers: ["gleam.toml"],
336
+ },
337
+
338
+ // =========================================================================
339
+ // Ruby
340
+ // =========================================================================
341
+
342
+ solargraph: {
343
+ command: "solargraph",
344
+ args: ["stdio"],
345
+ fileTypes: [".rb", ".rake", ".gemspec"],
346
+ rootMarkers: ["Gemfile", ".solargraph.yml", "Rakefile"],
347
+ initOptions: {
348
+ formatting: true,
349
+ },
350
+ settings: {
351
+ solargraph: {
352
+ diagnostics: true,
353
+ completion: true,
354
+ hover: true,
355
+ formatting: true,
356
+ references: true,
357
+ rename: true,
358
+ symbols: true,
359
+ },
360
+ },
361
+ },
362
+
363
+ "ruby-lsp": {
364
+ command: "ruby-lsp",
365
+ args: [],
366
+ fileTypes: [".rb", ".rake", ".gemspec", ".erb"],
367
+ rootMarkers: ["Gemfile", ".ruby-version", ".ruby-gemset"],
368
+ initOptions: {
369
+ formatter: "auto",
370
+ },
59
371
  },
372
+
373
+ rubocop: {
374
+ command: "rubocop",
375
+ args: ["--lsp"],
376
+ fileTypes: [".rb", ".rake"],
377
+ rootMarkers: [".rubocop.yml", "Gemfile"],
378
+ isLinter: true,
379
+ },
380
+
381
+ // =========================================================================
382
+ // Shell / Scripting
383
+ // =========================================================================
384
+
385
+ bashls: {
386
+ command: "bash-language-server",
387
+ args: ["start"],
388
+ fileTypes: [".sh", ".bash", ".zsh"],
389
+ rootMarkers: [".git"],
390
+ settings: {
391
+ bashIde: {
392
+ globPattern: "*@(.sh|.inc|.bash|.command)",
393
+ },
394
+ },
395
+ },
396
+
397
+ nushell: {
398
+ command: "nu",
399
+ args: ["--lsp"],
400
+ fileTypes: [".nu"],
401
+ rootMarkers: [".git"],
402
+ },
403
+
404
+ // =========================================================================
405
+ // Lua
406
+ // =========================================================================
407
+
60
408
  "lua-language-server": {
61
409
  command: "lua-language-server",
62
410
  args: [],
63
411
  fileTypes: [".lua"],
64
- rootMarkers: [".luarc.json", ".luarc.jsonc", ".luacheckrc"],
412
+ rootMarkers: [".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml"],
413
+ settings: {
414
+ Lua: {
415
+ runtime: { version: "LuaJIT" },
416
+ diagnostics: { globals: ["vim"] },
417
+ workspace: { checkThirdParty: false },
418
+ telemetry: { enable: false },
419
+ },
420
+ },
421
+ },
422
+
423
+ // =========================================================================
424
+ // PHP
425
+ // =========================================================================
426
+
427
+ intelephense: {
428
+ command: "intelephense",
429
+ args: ["--stdio"],
430
+ fileTypes: [".php", ".phtml"],
431
+ rootMarkers: ["composer.json", "composer.lock", ".git"],
432
+ },
433
+
434
+ phpactor: {
435
+ command: "phpactor",
436
+ args: ["language-server"],
437
+ fileTypes: [".php"],
438
+ rootMarkers: ["composer.json", ".phpactor.json", ".phpactor.yml"],
439
+ },
440
+
441
+ // =========================================================================
442
+ // .NET
443
+ // =========================================================================
444
+
445
+ omnisharp: {
446
+ command: "omnisharp",
447
+ args: ["-z", "--hostPID", String(process.pid), "--encoding", "utf-8", "--languageserver"],
448
+ fileTypes: [".cs", ".csx"],
449
+ rootMarkers: ["*.sln", "*.csproj", "omnisharp.json", ".git"],
450
+ settings: {
451
+ FormattingOptions: { EnableEditorConfigSupport: true },
452
+ RoslynExtensionsOptions: { EnableAnalyzersSupport: true },
453
+ },
454
+ },
455
+
456
+ // =========================================================================
457
+ // Configuration Languages
458
+ // =========================================================================
459
+
460
+ yamlls: {
461
+ command: "yaml-language-server",
462
+ args: ["--stdio"],
463
+ fileTypes: [".yaml", ".yml"],
464
+ rootMarkers: [".git"],
465
+ settings: {
466
+ yaml: {
467
+ validate: true,
468
+ format: { enable: true },
469
+ hover: true,
470
+ completion: true,
471
+ },
472
+ redhat: { telemetry: { enabled: false } },
473
+ },
474
+ },
475
+
476
+ taplo: {
477
+ command: "taplo",
478
+ args: ["lsp", "stdio"],
479
+ fileTypes: [".toml"],
480
+ rootMarkers: [".taplo.toml", "taplo.toml", ".git"],
481
+ },
482
+
483
+ terraformls: {
484
+ command: "terraform-ls",
485
+ args: ["serve"],
486
+ fileTypes: [".tf", ".tfvars"],
487
+ rootMarkers: [".terraform", "terraform.tfstate", "*.tf"],
488
+ },
489
+
490
+ dockerls: {
491
+ command: "docker-langserver",
492
+ args: ["--stdio"],
493
+ fileTypes: [".dockerfile"],
494
+ rootMarkers: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore"],
495
+ },
496
+
497
+ "helm-ls": {
498
+ command: "helm_ls",
499
+ args: ["serve"],
500
+ fileTypes: [".yaml", ".yml", ".tpl"],
501
+ rootMarkers: ["Chart.yaml", "Chart.yml"],
502
+ },
503
+
504
+ // =========================================================================
505
+ // Nix
506
+ // =========================================================================
507
+
508
+ nixd: {
509
+ command: "nixd",
510
+ args: [],
511
+ fileTypes: [".nix"],
512
+ rootMarkers: ["flake.nix", "default.nix", "shell.nix"],
513
+ },
514
+
515
+ nil: {
516
+ command: "nil",
517
+ args: [],
518
+ fileTypes: [".nix"],
519
+ rootMarkers: ["flake.nix", "default.nix", "shell.nix"],
520
+ },
521
+
522
+ // =========================================================================
523
+ // Other Languages
524
+ // =========================================================================
525
+
526
+ ols: {
527
+ command: "ols",
528
+ args: [],
529
+ fileTypes: [".odin"],
530
+ rootMarkers: ["ols.json", ".git"],
531
+ },
532
+
533
+ dartls: {
534
+ command: "dart",
535
+ args: ["language-server", "--protocol=lsp"],
536
+ fileTypes: [".dart"],
537
+ rootMarkers: ["pubspec.yaml", "pubspec.lock"],
538
+ initOptions: {
539
+ closingLabels: true,
540
+ flutterOutline: true,
541
+ outline: true,
542
+ },
543
+ },
544
+
545
+ marksman: {
546
+ command: "marksman",
547
+ args: ["server"],
548
+ fileTypes: [".md", ".markdown"],
549
+ rootMarkers: [".marksman.toml", ".git"],
550
+ },
551
+
552
+ texlab: {
553
+ command: "texlab",
554
+ args: [],
555
+ fileTypes: [".tex", ".bib", ".sty", ".cls"],
556
+ rootMarkers: [".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot", "Tectonic.toml"],
557
+ settings: {
558
+ texlab: {
559
+ build: {
560
+ executable: "latexmk",
561
+ args: ["-pdf", "-interaction=nonstopmode", "-synctex=1", "%f"],
562
+ },
563
+ chktex: { onOpenAndSave: true },
564
+ },
565
+ },
566
+ },
567
+
568
+ graphql: {
569
+ command: "graphql-lsp",
570
+ args: ["server", "-m", "stream"],
571
+ fileTypes: [".graphql", ".gql"],
572
+ rootMarkers: [".graphqlrc", ".graphqlrc.json", ".graphqlrc.yml", ".graphqlrc.yaml", "graphql.config.js"],
573
+ },
574
+
575
+ prismals: {
576
+ command: "prisma-language-server",
577
+ args: ["--stdio"],
578
+ fileTypes: [".prisma"],
579
+ rootMarkers: ["schema.prisma", "prisma/schema.prisma"],
580
+ },
581
+
582
+ vimls: {
583
+ command: "vim-language-server",
584
+ args: ["--stdio"],
585
+ fileTypes: [".vim", ".vimrc"],
586
+ rootMarkers: [".git"],
587
+ initOptions: {
588
+ isNeovim: true,
589
+ diagnostic: { enable: true },
590
+ },
591
+ },
592
+
593
+ // =========================================================================
594
+ // Emmet (HTML/CSS expansion)
595
+ // =========================================================================
596
+
597
+ "emmet-language-server": {
598
+ command: "emmet-language-server",
599
+ args: ["--stdio"],
600
+ fileTypes: [".html", ".css", ".scss", ".less", ".jsx", ".tsx", ".vue", ".svelte"],
601
+ rootMarkers: [".git"],
65
602
  },
66
603
  };
67
604
 
605
+ // =============================================================================
606
+ // Configuration Loading
607
+ // =============================================================================
608
+
68
609
  /**
69
610
  * Check if any root marker file exists in the directory
70
611
  */
71
612
  export function hasRootMarkers(cwd: string, markers: string[]): boolean {
72
- return markers.some((marker) => existsSync(join(cwd, marker)));
613
+ return markers.some((marker) => {
614
+ // Handle glob-like patterns (e.g., "*.cabal")
615
+ if (marker.includes("*")) {
616
+ try {
617
+ const { globSync } = require("node:fs");
618
+ const matches = globSync(join(cwd, marker));
619
+ return matches.length > 0;
620
+ } catch {
621
+ // globSync not available, skip glob patterns
622
+ return false;
623
+ }
624
+ }
625
+ return existsSync(join(cwd, marker));
626
+ });
627
+ }
628
+
629
+ // =============================================================================
630
+ // Local Binary Resolution
631
+ // =============================================================================
632
+
633
+ /**
634
+ * Local bin directories to check before $PATH, ordered by priority.
635
+ * Each entry maps a root marker to the bin directory to check.
636
+ */
637
+ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
638
+ // Node.js - check node_modules/.bin/
639
+ { markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
640
+ // Python - check virtual environment bin directories
641
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" },
642
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" },
643
+ { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" },
644
+ // Ruby - check vendor bundle and binstubs
645
+ { markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
646
+ { markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
647
+ // Go - check project-local bin
648
+ { markers: ["go.mod", "go.sum"], binDir: "bin" },
649
+ ];
650
+
651
+ /**
652
+ * Resolve a command to an executable path.
653
+ * Checks project-local bin directories first, then falls back to $PATH.
654
+ *
655
+ * @param command - The command name (e.g., "typescript-language-server")
656
+ * @param cwd - Working directory to search from
657
+ * @returns Absolute path to the executable, or null if not found
658
+ */
659
+ export function resolveCommand(command: string, cwd: string): string | null {
660
+ // Check local bin directories based on project markers
661
+ for (const { markers, binDir } of LOCAL_BIN_PATHS) {
662
+ if (hasRootMarkers(cwd, markers)) {
663
+ const localPath = join(cwd, binDir, command);
664
+ if (existsSync(localPath)) {
665
+ return localPath;
666
+ }
667
+ }
668
+ }
669
+
670
+ // Fall back to $PATH
671
+ return Bun.which(command);
672
+ }
673
+
674
+ /**
675
+ * Configuration file search paths (in priority order).
676
+ * Supports both visible and hidden variants, and both .pi subdirectory and root.
677
+ */
678
+ function getConfigPaths(cwd: string): string[] {
679
+ return [
680
+ // Project-level configs (highest priority)
681
+ join(cwd, "lsp.json"),
682
+ join(cwd, ".lsp.json"),
683
+ join(cwd, ".pi", "lsp.json"),
684
+ join(cwd, ".pi", ".lsp.json"),
685
+ // User-level configs (fallback)
686
+ join(homedir(), ".pi", "lsp.json"),
687
+ join(homedir(), ".pi", ".lsp.json"),
688
+ join(homedir(), "lsp.json"),
689
+ join(homedir(), ".lsp.json"),
690
+ ];
73
691
  }
74
692
 
75
693
  /**
76
694
  * Load LSP configuration.
77
695
  *
78
696
  * Priority:
79
- * 1. Project-level config from .pi/lsp.json in cwd
80
- * 2. User-level config from ~/.pi/lsp.json
697
+ * 1. Project-level config: lsp.json, .lsp.json, .pi/lsp.json, .pi/.lsp.json
698
+ * 2. User-level config: ~/.pi/lsp.json, ~/.pi/.lsp.json, ~/lsp.json, ~/.lsp.json
81
699
  * 3. Auto-detect from project markers + available binaries
700
+ *
701
+ * Config file format:
702
+ * ```json
703
+ * {
704
+ * "servers": {
705
+ * "typescript-language-server": {
706
+ * "command": "typescript-language-server",
707
+ * "args": ["--stdio", "--log-level", "4"],
708
+ * "disabled": false
709
+ * },
710
+ * "my-custom-server": {
711
+ * "command": "/path/to/server",
712
+ * "args": ["--stdio"],
713
+ * "fileTypes": [".xyz"],
714
+ * "rootMarkers": [".xyz-project"]
715
+ * }
716
+ * }
717
+ * }
718
+ * ```
82
719
  */
83
720
  export function loadConfig(cwd: string): LspConfig {
84
- // Try to load user config
85
- const configPaths = [join(cwd, ".pi", "lsp.json"), join(homedir(), ".pi", "lsp.json")];
721
+ const configPaths = getConfigPaths(cwd);
86
722
 
87
723
  for (const configPath of configPaths) {
88
724
  if (existsSync(configPath)) {
89
725
  try {
90
726
  const content = readFileSync(configPath, "utf-8");
91
727
  const parsed = JSON.parse(content);
728
+
729
+ // Support both { servers: {...} } and direct server map
92
730
  const servers = parsed.servers || parsed;
93
731
 
732
+ // Merge with defaults and filter to available
733
+ const merged: Record<string, ServerConfig> = { ...SERVERS };
734
+
735
+ for (const [name, config] of Object.entries(servers) as [string, Partial<ServerConfig>][]) {
736
+ if (merged[name]) {
737
+ // Merge with existing config
738
+ merged[name] = { ...merged[name], ...config };
739
+ } else {
740
+ // Add new server config
741
+ merged[name] = config as ServerConfig;
742
+ }
743
+ }
744
+
94
745
  // Filter to only enabled servers with available commands
95
746
  const available: Record<string, ServerConfig> = {};
96
- for (const [name, config] of Object.entries(servers) as [string, ServerConfig][]) {
747
+ for (const [name, config] of Object.entries(merged)) {
97
748
  if (config.disabled) continue;
98
- if (!Bun.which(config.command)) continue;
99
- available[name] = config;
749
+ const resolved = resolveCommand(config.command, cwd);
750
+ if (!resolved) continue;
751
+ available[name] = { ...config, resolvedCommand: resolved };
100
752
  }
101
753
 
102
754
  return { servers: available };
103
755
  } catch {
104
- // Ignore parse errors, fall through to auto-detect
756
+ // Ignore parse errors, continue to next config or auto-detect
105
757
  }
106
758
  }
107
759
  }
@@ -113,27 +765,49 @@ export function loadConfig(cwd: string): LspConfig {
113
765
  // Check if project has root markers for this language
114
766
  if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
115
767
 
116
- // Check if the language server binary is available
117
- if (!Bun.which(config.command)) continue;
768
+ // Check if the language server binary is available (local or $PATH)
769
+ const resolved = resolveCommand(config.command, cwd);
770
+ if (!resolved) continue;
118
771
 
119
- detected[name] = config;
772
+ detected[name] = { ...config, resolvedCommand: resolved };
120
773
  }
121
774
 
122
775
  return { servers: detected };
123
776
  }
124
777
 
778
+ // =============================================================================
779
+ // Server Selection
780
+ // =============================================================================
781
+
125
782
  /**
126
- * Find the appropriate server for a file based on extension
783
+ * Find all servers that can handle a file based on extension.
784
+ * Returns servers sorted with primary (non-linter) servers first.
127
785
  */
128
- export function getServerForFile(config: LspConfig, filePath: string): [string, ServerConfig] | null {
786
+ export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
129
787
  const ext = extname(filePath).toLowerCase();
788
+ const matches: Array<[string, ServerConfig]> = [];
130
789
 
131
790
  for (const [name, serverConfig] of Object.entries(config.servers)) {
132
791
  if (serverConfig.fileTypes.includes(ext)) {
133
- return [name, serverConfig];
792
+ matches.push([name, serverConfig]);
134
793
  }
135
794
  }
136
- return null;
795
+
796
+ // Sort: primary servers (non-linters) first, then linters
797
+ return matches.sort((a, b) => {
798
+ const aIsLinter = a[1].isLinter ? 1 : 0;
799
+ const bIsLinter = b[1].isLinter ? 1 : 0;
800
+ return aIsLinter - bIsLinter;
801
+ });
802
+ }
803
+
804
+ /**
805
+ * Find the primary server for a file (prefers type-checkers over linters).
806
+ * Used for operations like definition, hover, references that need type intelligence.
807
+ */
808
+ export function getServerForFile(config: LspConfig, filePath: string): [string, ServerConfig] | null {
809
+ const servers = getServersForFile(config, filePath);
810
+ return servers.length > 0 ? servers[0] : null;
137
811
  }
138
812
 
139
813
  /**