crabcode 0.1.6__py3-none-win_amd64.whl

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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,583 @@
1
+ """LSP manager — main entry point for the LSP subsystem.
2
+
3
+ Responsibilities:
4
+ - Load config and merge built-in + custom LSP server definitions.
5
+ - Lazy-start clients via :meth:`get_clients` which matches file extensions,
6
+ resolves the project root, spawns an :class:`LSPClient`, and caches it.
7
+ - Track broken servers (failed to start — skip retries).
8
+ - Track spawning clients (prevent concurrent starts for the same key).
9
+ - Expose ``touch_file``, ``diagnostics``, ``hover``, ``definition``,
10
+ ``references`` methods that fan out to the appropriate clients.
11
+ - ``shutdown()`` to close all active clients.
12
+
13
+ Key invariants:
14
+ - Same ``(root_uri, server_id)`` pair gets only one client instance.
15
+ - One file may match multiple servers (e.g. ``.ts`` gets both *typescript*
16
+ and *eslint*).
17
+ - :class:`LSPManager` is session-scoped (one per :class:`CoreSession`).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import os
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from crabcode_core.logging_utils import get_logger
28
+ from crabcode_core.io_worker import run_io
29
+ from crabcode_core.lsp.client import LSPClient, _path_to_uri
30
+ from crabcode_core.types.config import CrabCodeSettings, LspServerConfig
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Built-in LSP server definitions
37
+ # ---------------------------------------------------------------------------
38
+
39
+ BUILTIN_LSP_SERVERS: dict[str, LspServerConfig] = {
40
+ "python": LspServerConfig(
41
+ command=["pyright-langserver", "--stdio"],
42
+ extensions=[".py", ".pyi", ".pyw"],
43
+ ),
44
+ "typescript": LspServerConfig(
45
+ command=["typescript-language-server", "--stdio"],
46
+ extensions=[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
47
+ ),
48
+ "go": LspServerConfig(
49
+ command=["gopls"],
50
+ extensions=[".go"],
51
+ ),
52
+ "rust": LspServerConfig(
53
+ command=["rust-analyzer"],
54
+ extensions=[".rs"],
55
+ ),
56
+ "csharp": LspServerConfig(
57
+ command=["omnisharp", "--stdio"],
58
+ extensions=[".cs"],
59
+ ),
60
+ "java": LspServerConfig(
61
+ command=["jdtls"],
62
+ extensions=[".java"],
63
+ ),
64
+ "cpp": LspServerConfig(
65
+ command=["clangd"],
66
+ extensions=[".c", ".cpp", ".h", ".hpp", ".cc", ".cxx", ".hh", ".hxx"],
67
+ ),
68
+ "ruby": LspServerConfig(
69
+ command=["solargraph", "stdio"],
70
+ extensions=[".rb"],
71
+ ),
72
+ "php": LspServerConfig(
73
+ command=["phpactor", "language-server"],
74
+ extensions=[".php"],
75
+ ),
76
+ "elixir": LspServerConfig(
77
+ command=["lexical"],
78
+ extensions=[".ex", ".exs", ".elixir"],
79
+ ),
80
+ "dart": LspServerConfig(
81
+ command=["dart", "language-server", "--protocol=lsp"],
82
+ extensions=[".dart"],
83
+ ),
84
+ "lua": LspServerConfig(
85
+ command=["lua-language-server"],
86
+ extensions=[".lua"],
87
+ ),
88
+ "kotlin": LspServerConfig(
89
+ command=["kotlin-language-server"],
90
+ extensions=[".kt", ".kts"],
91
+ ),
92
+ "swift": LspServerConfig(
93
+ command=["sourcekit-lsp"],
94
+ extensions=[".swift"],
95
+ ),
96
+ "zig": LspServerConfig(
97
+ command=["zls"],
98
+ extensions=[".zig"],
99
+ ),
100
+ "scala": LspServerConfig(
101
+ command=["metals"],
102
+ extensions=[".scala"],
103
+ ),
104
+ }
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Root-finding helpers
109
+ # ---------------------------------------------------------------------------
110
+
111
+ _ROOT_MARKERS: list[str] = [
112
+ ".git",
113
+ ".hg",
114
+ ".svn",
115
+ # Language-specific
116
+ "pyproject.toml",
117
+ "setup.py",
118
+ "setup.cfg",
119
+ "Cargo.toml",
120
+ "go.mod",
121
+ "package.json",
122
+ "tsconfig.json",
123
+ "jsconfig.json",
124
+ "*.sln",
125
+ "*.csproj",
126
+ "pom.xml",
127
+ "build.gradle",
128
+ "build.gradle.kts",
129
+ "Gemfile",
130
+ "mix.exs",
131
+ "pubspec.yaml",
132
+ "CMakeLists.txt",
133
+ "Makefile",
134
+ ]
135
+
136
+
137
+ def find_project_root(file_path: str | Path) -> str:
138
+ """Walk upward from *file_path* to find the project root directory.
139
+
140
+ The root is the nearest ancestor directory containing one of the
141
+ conventional root markers (``.git``, ``pyproject.toml``, …). Falls back
142
+ to the dirname of *file_path* itself.
143
+ """
144
+ current = Path(file_path).resolve()
145
+ if current.is_file():
146
+ current = current.parent
147
+
148
+ for _ in range(64): # safety bound
149
+ for marker in _ROOT_MARKERS:
150
+ if marker.startswith("*"):
151
+ # Glob-style marker — e.g. *.sln
152
+ if any(current.glob(marker)):
153
+ return str(current)
154
+ else:
155
+ if (current / marker).exists():
156
+ return str(current)
157
+ parent = current.parent
158
+ if parent == current:
159
+ break
160
+ current = parent
161
+
162
+ return str(current)
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # LSPManager
167
+ # ---------------------------------------------------------------------------
168
+
169
+ # Type alias for the cache key: (root_uri, server_id)
170
+ _ClientKey = tuple[str, str]
171
+
172
+
173
+ class LSPManager:
174
+ """Session-scoped manager for LSP client instances.
175
+
176
+ Usage::
177
+
178
+ manager = LSPManager(cwd="/path/to/project", settings=settings)
179
+ clients = await manager.get_clients("src/main.py")
180
+ for client in clients:
181
+ await client.touch_file("src/main.py")
182
+ diag = await manager.diagnostics("src/main.py")
183
+ await manager.shutdown()
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ cwd: str = ".",
189
+ settings: CrabCodeSettings | None = None,
190
+ ) -> None:
191
+ self._cwd = os.path.abspath(cwd)
192
+ self._settings = settings or CrabCodeSettings()
193
+
194
+ # Merged server configs: {server_id: LspServerConfig}
195
+ self._servers: dict[str, LspServerConfig] = {}
196
+ self._load_servers()
197
+
198
+ # Active client cache: {(root_uri, server_id): LSPClient}
199
+ self._clients: dict[_ClientKey, LSPClient] = {}
200
+
201
+ # Broken server set: {(root_uri, server_id)} — skip future starts
202
+ self._broken: set[_ClientKey] = set()
203
+
204
+ # Currently spawning: {(root_uri, server_id): asyncio.Task}
205
+ # Prevents concurrent starts for the same key.
206
+ self._spawning: dict[_ClientKey, asyncio.Task[LSPClient]] = {}
207
+
208
+ # -------------------------------------------------------------------
209
+ # Config loading
210
+ # -------------------------------------------------------------------
211
+
212
+ def _load_servers(self) -> None:
213
+ """Merge built-in and user-configured LSP servers.
214
+
215
+ User config can override built-in entries (same key = override).
216
+ Setting ``disabled: true`` on a built-in server removes it.
217
+ """
218
+ merged: dict[str, LspServerConfig] = {}
219
+
220
+ # 1. Start with built-in defaults
221
+ for server_id, config in BUILTIN_LSP_SERVERS.items():
222
+ merged[server_id] = config
223
+
224
+ # 2. Merge user/custom from settings
225
+ user_lsp = self._settings.lsp
226
+ if isinstance(user_lsp, bool):
227
+ # ``lsp: false`` → disable all built-in servers
228
+ if not user_lsp:
229
+ merged.clear()
230
+ elif isinstance(user_lsp, dict):
231
+ for server_id, config in user_lsp.items():
232
+ if config.disabled and server_id in merged:
233
+ # Explicitly disable a built-in server
234
+ del merged[server_id]
235
+ continue
236
+ if not config.command and server_id in merged:
237
+ # No command provided — keep built-in unless disabled
238
+ continue
239
+ merged[server_id] = config
240
+
241
+ # Remove any entries with empty command
242
+ self._servers = {
243
+ sid: cfg for sid, cfg in merged.items() if cfg.command
244
+ }
245
+
246
+ # -------------------------------------------------------------------
247
+ # Extension matching
248
+ # -------------------------------------------------------------------
249
+
250
+ def _matching_servers(self, file_path: str | Path) -> list[str]:
251
+ """Return server IDs whose ``extensions`` list matches *file_path*."""
252
+ suffix = Path(file_path).suffix.lower()
253
+ if not suffix:
254
+ return []
255
+
256
+ matched: list[str] = []
257
+ for server_id, config in self._servers.items():
258
+ if suffix in config.extensions:
259
+ matched.append(server_id)
260
+ return matched
261
+
262
+ # -------------------------------------------------------------------
263
+ # Client lifecycle
264
+ # -------------------------------------------------------------------
265
+
266
+ async def get_clients(self, file_path: str | Path) -> list[LSPClient]:
267
+ """Return all LSP clients relevant to *file_path*.
268
+
269
+ For each matching server:
270
+ 1. Compute the project root.
271
+ 2. Build the cache key ``(root_uri, server_id)``.
272
+ 3. If a client is cached, return it.
273
+ 4. If the key is broken, skip it.
274
+ 5. If a spawn is in progress, await it.
275
+ 6. Otherwise, start a new client and cache it.
276
+ """
277
+ file_path = str(file_path)
278
+ matching = self._matching_servers(file_path)
279
+ if not matching:
280
+ return []
281
+
282
+ root = await run_io(__name__, "find_project_root", file_path, timeout=5)
283
+ root_uri = _path_to_uri(root)
284
+
285
+ clients: list[LSPClient] = []
286
+ tasks: list[tuple[_ClientKey, asyncio.Task[LSPClient]]] = []
287
+
288
+ for server_id in matching:
289
+ key = (root_uri, server_id)
290
+
291
+ # Already running
292
+ if key in self._clients:
293
+ clients.append(self._clients[key])
294
+ continue
295
+
296
+ # Previously broken — skip
297
+ if key in self._broken:
298
+ continue
299
+
300
+ # Currently spawning — wait for it
301
+ if key in self._spawning:
302
+ tasks.append((key, self._spawning[key]))
303
+ continue
304
+
305
+ # Start a new client
306
+ config = self._servers[server_id]
307
+
308
+ async def _start(
309
+ cfg: LspServerConfig = config,
310
+ k: _ClientKey = key,
311
+ sid: str = server_id,
312
+ ru: str = root_uri,
313
+ ) -> LSPClient:
314
+ return await self._start_client(cfg, k, sid, ru)
315
+
316
+ task = asyncio.create_task(_start())
317
+ self._spawning[key] = task
318
+ tasks.append((key, task))
319
+
320
+ # Await all pending spawns
321
+ for key, task in tasks:
322
+ try:
323
+ client = await task
324
+ clients.append(client)
325
+ except Exception:
326
+ logger.warning(
327
+ "LSP client failed to start for %s at %s",
328
+ key[1], key[0],
329
+ exc_info=True,
330
+ )
331
+
332
+ return clients
333
+
334
+ async def _start_client(
335
+ self,
336
+ config: LspServerConfig,
337
+ key: _ClientKey,
338
+ server_id: str,
339
+ root_uri: str,
340
+ ) -> LSPClient:
341
+ """Start a single LSP client, update caches, and return it."""
342
+ try:
343
+ client = LSPClient(
344
+ command=config.command,
345
+ root_uri=root_uri,
346
+ env=config.env or None,
347
+ initialization_options=config.initialization or None,
348
+ )
349
+ await client.create()
350
+ self._clients[key] = client
351
+ logger.info(
352
+ "LSP client started: %s (root=%s)",
353
+ server_id, root_uri,
354
+ )
355
+ return client
356
+ except Exception:
357
+ self._broken.add(key)
358
+ raise
359
+ finally:
360
+ self._spawning.pop(key, None)
361
+
362
+ # -------------------------------------------------------------------
363
+ # Convenience methods (fan-out to all matching clients)
364
+ # -------------------------------------------------------------------
365
+
366
+ async def touch_file(
367
+ self,
368
+ file_path: str | Path,
369
+ text: str | None = None,
370
+ ) -> None:
371
+ """Open or update *file_path* in every matching LSP client."""
372
+ clients = await self.get_clients(file_path)
373
+ for client in clients:
374
+ try:
375
+ await client.touch_file(file_path, text=text)
376
+ except Exception:
377
+ logger.warning(
378
+ "LSP touch_file failed for %s", file_path,
379
+ exc_info=True,
380
+ )
381
+
382
+ async def diagnostics(
383
+ self,
384
+ file_path: str | Path,
385
+ *,
386
+ timeout: float = 30.0,
387
+ debounce: float = 0.5,
388
+ ) -> list[dict[str, Any]]:
389
+ """Collect diagnostics for *file_path* from all matching clients.
390
+
391
+ Returns a merged list of diagnostic dicts. Each dict includes an
392
+ extra ``"source"`` key indicating the server that produced it.
393
+ """
394
+ clients = await self.get_clients(file_path)
395
+ all_diags: list[dict[str, Any]] = []
396
+
397
+ for client in clients:
398
+ try:
399
+ diags = await client.wait_for_diagnostics(
400
+ file_path, timeout=timeout, debounce=debounce,
401
+ )
402
+ for d in diags:
403
+ all_diags.append({**d, "source": _client_source(client)})
404
+ except Exception:
405
+ logger.warning(
406
+ "LSP diagnostics failed for %s", file_path,
407
+ exc_info=True,
408
+ )
409
+
410
+ return all_diags
411
+
412
+ async def hover(
413
+ self,
414
+ file_path: str | Path,
415
+ line: int,
416
+ character: int,
417
+ ) -> list[dict[str, Any]]:
418
+ """Request hover information at (*line*, *character*) from matching clients.
419
+
420
+ *line* and *character* are 0-based as per the LSP spec.
421
+
422
+ Returns a list of hover results (one per server), each with an extra
423
+ ``"source"`` key.
424
+ """
425
+ clients = await self.get_clients(file_path)
426
+ results: list[dict[str, Any]] = []
427
+
428
+ uri = _path_to_uri(str(file_path))
429
+ params = {
430
+ "textDocument": {"uri": uri},
431
+ "position": {"line": line, "character": character},
432
+ }
433
+
434
+ for client in clients:
435
+ try:
436
+ result = await client.send_request("textDocument/hover", params)
437
+ if result:
438
+ results.append({**result, "source": _client_source(client)})
439
+ except Exception:
440
+ logger.debug(
441
+ "LSP hover failed for %s at %d:%d",
442
+ file_path, line, character,
443
+ exc_info=True,
444
+ )
445
+
446
+ return results
447
+
448
+ async def definition(
449
+ self,
450
+ file_path: str | Path,
451
+ line: int,
452
+ character: int,
453
+ ) -> list[dict[str, Any]]:
454
+ """Request go-to-definition at (*line*, *character*) from matching clients.
455
+
456
+ *line* and *character* are 0-based.
457
+
458
+ Returns a list of location / location-link results with ``"source"``.
459
+ """
460
+ clients = await self.get_clients(file_path)
461
+ results: list[dict[str, Any]] = []
462
+
463
+ uri = _path_to_uri(str(file_path))
464
+ params = {
465
+ "textDocument": {"uri": uri},
466
+ "position": {"line": line, "character": character},
467
+ }
468
+
469
+ for client in clients:
470
+ try:
471
+ result = await client.send_request(
472
+ "textDocument/definition", params,
473
+ )
474
+ if result:
475
+ results.append({**result, "source": _client_source(client)})
476
+ except Exception:
477
+ logger.debug(
478
+ "LSP definition failed for %s at %d:%d",
479
+ file_path, line, character,
480
+ exc_info=True,
481
+ )
482
+
483
+ return results
484
+
485
+ async def references(
486
+ self,
487
+ file_path: str | Path,
488
+ line: int,
489
+ character: int,
490
+ *,
491
+ include_declaration: bool = True,
492
+ ) -> list[dict[str, Any]]:
493
+ """Request find-references at (*line*, *character*) from matching clients.
494
+
495
+ *line* and *character* are 0-based.
496
+
497
+ Returns a list of reference location results with ``"source"``.
498
+ """
499
+ clients = await self.get_clients(file_path)
500
+ results: list[dict[str, Any]] = []
501
+
502
+ uri = _path_to_uri(str(file_path))
503
+ params = {
504
+ "textDocument": {"uri": uri},
505
+ "position": {"line": line, "character": character},
506
+ "context": {"includeDeclaration": include_declaration},
507
+ }
508
+
509
+ for client in clients:
510
+ try:
511
+ result = await client.send_request(
512
+ "textDocument/references", params,
513
+ )
514
+ if result:
515
+ results.append({**result, "source": _client_source(client)})
516
+ except Exception:
517
+ logger.debug(
518
+ "LSP references failed for %s at %d:%d",
519
+ file_path, line, character,
520
+ exc_info=True,
521
+ )
522
+
523
+ return results
524
+
525
+ # -------------------------------------------------------------------
526
+ # Shutdown
527
+ # -------------------------------------------------------------------
528
+
529
+ async def shutdown(self) -> None:
530
+ """Close all active LSP clients and clear caches."""
531
+ # Cancel any in-progress spawns
532
+ for key, task in self._spawning.items():
533
+ task.cancel()
534
+ self._spawning.clear()
535
+
536
+ # Shut down active clients
537
+ for key, client in self._clients.items():
538
+ try:
539
+ await client.shutdown()
540
+ except Exception:
541
+ logger.warning(
542
+ "LSP client shutdown failed for %s at %s",
543
+ key[1], key[0],
544
+ exc_info=True,
545
+ )
546
+
547
+ self._clients.clear()
548
+ self._broken.clear()
549
+ logger.info("LSP manager shut down")
550
+
551
+ # -------------------------------------------------------------------
552
+ # Introspection
553
+ # -------------------------------------------------------------------
554
+
555
+ @property
556
+ def servers(self) -> dict[str, LspServerConfig]:
557
+ """Read-only view of the merged server configurations."""
558
+ return dict(self._servers)
559
+
560
+ @property
561
+ def active_clients(self) -> dict[str, LSPClient]:
562
+ """Active clients keyed by ``"root_uri|server_id"``."""
563
+ return {
564
+ f"{root_uri}|{server_id}": client
565
+ for (root_uri, server_id), client in self._clients.items()
566
+ }
567
+
568
+ @property
569
+ def broken_servers(self) -> set[str]:
570
+ """Set of ``"root_uri|server_id"`` strings for broken servers."""
571
+ return {f"{root_uri}|{server_id}" for root_uri, server_id in self._broken}
572
+
573
+
574
+ # ---------------------------------------------------------------------------
575
+ # Helpers
576
+ # ---------------------------------------------------------------------------
577
+
578
+
579
+ def _client_source(client: LSPClient) -> str:
580
+ """Derive a human-readable source label from a client's command."""
581
+ if client._command:
582
+ return Path(client._command[0]).name
583
+ return "unknown"
@@ -0,0 +1,4 @@
1
+ from crabcode_core.mcp.client import McpManager, McpToolWrapper
2
+ from crabcode_core.mcp.config import load_mcp_configs
3
+
4
+ __all__ = ["McpManager", "McpToolWrapper", "load_mcp_configs"]