llmcode-cli 1.0.0__py3-none-any.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 (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
llm_code/lsp/client.py ADDED
@@ -0,0 +1,298 @@
1
+ """LSP client: types, LspTransport, and LspClient."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import itertools
6
+ import json
7
+ import os
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Data types
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Location:
20
+ file: str
21
+ line: int
22
+ column: int
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Diagnostic:
27
+ file: str
28
+ line: int
29
+ column: int
30
+ severity: str # "error" | "warning" | "info" | "hint"
31
+ message: str
32
+ source: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class LspServerConfig:
37
+ command: str
38
+ args: tuple[str, ...] = ()
39
+ language: str = ""
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Transport abstraction
44
+ # ---------------------------------------------------------------------------
45
+
46
+ _SEVERITY_MAP: dict[int, str] = {
47
+ 1: "error",
48
+ 2: "warning",
49
+ 3: "info",
50
+ 4: "hint",
51
+ }
52
+
53
+
54
+ class LspTransport(ABC):
55
+ """Abstract base for LSP transports (Content-Length framed JSON-RPC)."""
56
+
57
+ @abstractmethod
58
+ async def start(self) -> None: ...
59
+
60
+ @abstractmethod
61
+ async def send_message(self, message: dict[str, Any]) -> None: ...
62
+
63
+ @abstractmethod
64
+ async def receive_message(self) -> dict[str, Any]: ...
65
+
66
+ @abstractmethod
67
+ async def close(self) -> None: ...
68
+
69
+
70
+ class StdioLspTransport(LspTransport):
71
+ """LSP transport over subprocess stdin/stdout using Content-Length framing."""
72
+
73
+ RECEIVE_TIMEOUT = 30.0
74
+ CLOSE_WAIT_TIMEOUT = 5.0
75
+
76
+ def __init__(
77
+ self,
78
+ command: str,
79
+ args: tuple[str, ...] = (),
80
+ ) -> None:
81
+ self._command = command
82
+ self._args = args
83
+ self._process: asyncio.subprocess.Process | None = None
84
+
85
+ async def start(self) -> None:
86
+ self._process = await asyncio.create_subprocess_exec(
87
+ self._command,
88
+ *self._args,
89
+ stdin=asyncio.subprocess.PIPE,
90
+ stdout=asyncio.subprocess.PIPE,
91
+ stderr=asyncio.subprocess.PIPE,
92
+ env={**os.environ},
93
+ )
94
+
95
+ async def send_message(self, message: dict[str, Any]) -> None:
96
+ if self._process is None or self._process.stdin is None:
97
+ raise RuntimeError("Transport not started")
98
+ body = json.dumps(message).encode("utf-8")
99
+ header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
100
+ self._process.stdin.write(header + body)
101
+ await self._process.stdin.drain()
102
+
103
+ async def receive_message(self) -> dict[str, Any]:
104
+ if self._process is None or self._process.stdout is None:
105
+ raise RuntimeError("Transport not started")
106
+
107
+ headers: dict[str, str] = {}
108
+ while True:
109
+ line = await asyncio.wait_for(
110
+ self._process.stdout.readline(),
111
+ timeout=self.RECEIVE_TIMEOUT,
112
+ )
113
+ line_str = line.decode("ascii").strip()
114
+ if not line_str:
115
+ break
116
+ key, _, value = line_str.partition(":")
117
+ headers[key.strip().lower()] = value.strip()
118
+
119
+ content_length = int(headers.get("content-length", "0"))
120
+ body = await asyncio.wait_for(
121
+ self._process.stdout.readexactly(content_length),
122
+ timeout=self.RECEIVE_TIMEOUT,
123
+ )
124
+ return json.loads(body.decode("utf-8"))
125
+
126
+ async def close(self) -> None:
127
+ if self._process is None:
128
+ return
129
+ process = self._process
130
+ self._process = None
131
+ try:
132
+ if process.stdin and not process.stdin.is_closing():
133
+ process.stdin.close()
134
+ except Exception:
135
+ pass
136
+ try:
137
+ process.terminate()
138
+ except (ProcessLookupError, OSError):
139
+ pass
140
+ try:
141
+ await asyncio.wait_for(process.wait(), timeout=self.CLOSE_WAIT_TIMEOUT)
142
+ except asyncio.TimeoutError:
143
+ try:
144
+ process.kill()
145
+ except (ProcessLookupError, OSError):
146
+ pass
147
+ try:
148
+ await process.wait()
149
+ except Exception:
150
+ pass
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # LSP client
155
+ # ---------------------------------------------------------------------------
156
+
157
+
158
+ class LspClient:
159
+ """High-level LSP client."""
160
+
161
+ def __init__(self, transport: LspTransport) -> None:
162
+ self._transport = transport
163
+ self._id_counter = itertools.count(1)
164
+
165
+ # ------------------------------------------------------------------
166
+ # Public API
167
+ # ------------------------------------------------------------------
168
+
169
+ async def initialize(self, root_uri: str) -> dict[str, Any]:
170
+ result = await self._request(
171
+ "initialize",
172
+ {
173
+ "processId": None,
174
+ "clientInfo": {"name": "llm-code", "version": "0.1.0"},
175
+ "rootUri": root_uri,
176
+ "capabilities": {},
177
+ },
178
+ )
179
+ return result
180
+
181
+ async def goto_definition(
182
+ self, file_uri: str, line: int, col: int
183
+ ) -> list[Location]:
184
+ result = await self._request(
185
+ "textDocument/definition",
186
+ {
187
+ "textDocument": {"uri": file_uri},
188
+ "position": {"line": line, "character": col},
189
+ },
190
+ )
191
+ return self._parse_locations(result)
192
+
193
+ async def find_references(
194
+ self, file_uri: str, line: int, col: int
195
+ ) -> list[Location]:
196
+ result = await self._request(
197
+ "textDocument/references",
198
+ {
199
+ "textDocument": {"uri": file_uri},
200
+ "position": {"line": line, "character": col},
201
+ "context": {"includeDeclaration": True},
202
+ },
203
+ )
204
+ return self._parse_locations(result)
205
+
206
+ async def get_diagnostics(self, file_uri: str) -> list[Diagnostic]:
207
+ result = await self._request(
208
+ "textDocument/diagnostic",
209
+ {"textDocument": {"uri": file_uri}},
210
+ )
211
+ diagnostics: list[Diagnostic] = []
212
+ items = result.get("items", [])
213
+ for item in items:
214
+ uri = item.get("uri", file_uri)
215
+ for raw in item.get("diagnostics", []):
216
+ start = raw.get("range", {}).get("start", {})
217
+ severity_int = raw.get("severity", 1)
218
+ severity = _SEVERITY_MAP.get(severity_int, "error")
219
+ diagnostics.append(
220
+ Diagnostic(
221
+ file=uri,
222
+ line=start.get("line", 0),
223
+ column=start.get("character", 0),
224
+ severity=severity,
225
+ message=raw.get("message", ""),
226
+ source=raw.get("source", ""),
227
+ )
228
+ )
229
+ return diagnostics
230
+
231
+ async def did_open(self, file_uri: str, text: str) -> None:
232
+ """Send textDocument/didOpen notification (no response expected)."""
233
+ await self._notify(
234
+ "textDocument/didOpen",
235
+ {
236
+ "textDocument": {
237
+ "uri": file_uri,
238
+ "languageId": "plaintext",
239
+ "version": 1,
240
+ "text": text,
241
+ }
242
+ },
243
+ )
244
+
245
+ async def shutdown(self) -> None:
246
+ """Send shutdown + exit, then close transport."""
247
+ await self._request("shutdown", {})
248
+ await self._notify("exit", {})
249
+ await self._transport.close()
250
+
251
+ # ------------------------------------------------------------------
252
+ # Internal helpers
253
+ # ------------------------------------------------------------------
254
+
255
+ async def _request(self, method: str, params: dict[str, Any]) -> Any:
256
+ request_id = next(self._id_counter)
257
+ message: dict[str, Any] = {
258
+ "jsonrpc": "2.0",
259
+ "id": request_id,
260
+ "method": method,
261
+ "params": params,
262
+ }
263
+ await self._transport.send_message(message)
264
+ response = await self._transport.receive_message()
265
+
266
+ if "error" in response:
267
+ error = response["error"]
268
+ raise RuntimeError(
269
+ f"LSP error {error.get('code')}: {error.get('message', 'Unknown error')}"
270
+ )
271
+ return response.get("result")
272
+
273
+ async def _notify(self, method: str, params: dict[str, Any]) -> None:
274
+ """Send a JSON-RPC notification (no id, no response)."""
275
+ message: dict[str, Any] = {
276
+ "jsonrpc": "2.0",
277
+ "method": method,
278
+ "params": params,
279
+ }
280
+ await self._transport.send_message(message)
281
+
282
+ def _parse_locations(self, result: Any) -> list[Location]:
283
+ if result is None:
284
+ return []
285
+ if isinstance(result, dict):
286
+ result = [result]
287
+ locations = []
288
+ for item in result:
289
+ uri = item.get("uri", "")
290
+ start = item.get("range", {}).get("start", {})
291
+ locations.append(
292
+ Location(
293
+ file=uri,
294
+ line=start.get("line", 0),
295
+ column=start.get("character", 0),
296
+ )
297
+ )
298
+ return locations
@@ -0,0 +1,42 @@
1
+ """LSP server auto-detector: discovers language servers from project marker files."""
2
+ from __future__ import annotations
3
+
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from llm_code.lsp.client import LspServerConfig
8
+
9
+ # Maps marker filename -> (command, args, language)
10
+ _DETECTORS: dict[str, tuple[str, list[str], str]] = {
11
+ "pyproject.toml": ("pyright-langserver", ["--stdio"], "python"),
12
+ "setup.py": ("pyright-langserver", ["--stdio"], "python"),
13
+ "requirements.txt": ("pyright-langserver", ["--stdio"], "python"),
14
+ "package.json": ("typescript-language-server", ["--stdio"], "typescript"),
15
+ "tsconfig.json": ("typescript-language-server", ["--stdio"], "typescript"),
16
+ "go.mod": ("gopls", ["serve"], "go"),
17
+ "Cargo.toml": ("rust-analyzer", [], "rust"),
18
+ }
19
+
20
+
21
+ def detect_lsp_servers(cwd: Path) -> dict[str, LspServerConfig]:
22
+ """Detect available LSP servers based on marker files in *cwd*.
23
+
24
+ Returns a mapping of language -> LspServerConfig.
25
+ Only includes languages where the server binary exists on PATH.
26
+ If multiple markers for the same language are found, the language
27
+ appears only once.
28
+ """
29
+ found: dict[str, LspServerConfig] = {}
30
+ for marker, (command, args, language) in _DETECTORS.items():
31
+ if language in found:
32
+ continue
33
+ if not (cwd / marker).exists():
34
+ continue
35
+ if shutil.which(command) is None:
36
+ continue
37
+ found[language] = LspServerConfig(
38
+ command=command,
39
+ args=tuple(args),
40
+ language=language,
41
+ )
42
+ return found
@@ -0,0 +1,56 @@
1
+ """LSP server manager: lifecycle management for multiple language servers."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from llm_code.lsp.client import LspClient, LspServerConfig, StdioLspTransport
7
+
8
+
9
+ class LspServerManager:
10
+ """Manages a pool of running LSP clients, keyed by language."""
11
+
12
+ def __init__(self) -> None:
13
+ self._clients: dict[str, LspClient] = {}
14
+
15
+ async def start_server(
16
+ self, name: str, config: LspServerConfig, root_path: Path
17
+ ) -> LspClient:
18
+ """Start a single LSP server and return a connected LspClient.
19
+
20
+ *name* is used as the key (typically the language name).
21
+ """
22
+ transport = StdioLspTransport(command=config.command, args=config.args)
23
+ await transport.start()
24
+ client = LspClient(transport)
25
+ root_uri = root_path.as_uri()
26
+ await client.initialize(root_uri)
27
+ self._clients[name] = client
28
+ return client
29
+
30
+ async def start_all(
31
+ self, configs: dict[str, LspServerConfig], root_path: Path
32
+ ) -> None:
33
+ """Start all servers from a language -> config mapping.
34
+
35
+ Failures are logged and skipped so one broken server does not block others.
36
+ """
37
+ for name, config in configs.items():
38
+ try:
39
+ await self.start_server(name, config, root_path)
40
+ except Exception as exc:
41
+ import warnings
42
+ warnings.warn(f"Failed to start LSP server '{name}': {exc}", stacklevel=2)
43
+
44
+ async def stop_all(self) -> None:
45
+ """Shutdown all running servers gracefully."""
46
+ clients = list(self._clients.values())
47
+ self._clients.clear()
48
+ for client in clients:
49
+ try:
50
+ await client.shutdown()
51
+ except Exception:
52
+ pass
53
+
54
+ def get_client(self, language: str) -> LspClient | None:
55
+ """Return the LspClient for *language*, or None if not running."""
56
+ return self._clients.get(language)
llm_code/lsp/tools.py ADDED
@@ -0,0 +1,288 @@
1
+ """LSP tools: goto-definition, find-references, diagnostics."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from llm_code.lsp.manager import LspServerManager
9
+ from llm_code.tools.base import PermissionLevel, Tool, ToolResult
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # File-extension -> language mapping
13
+ # ---------------------------------------------------------------------------
14
+
15
+ _EXT_LANGUAGE: dict[str, str] = {
16
+ ".py": "python",
17
+ ".pyi": "python",
18
+ ".ts": "typescript",
19
+ ".tsx": "typescript",
20
+ ".js": "typescript",
21
+ ".jsx": "typescript",
22
+ ".go": "go",
23
+ ".rs": "rust",
24
+ }
25
+
26
+
27
+ def _language_for_file(file_path: str) -> str:
28
+ suffix = Path(file_path).suffix.lower()
29
+ return _EXT_LANGUAGE.get(suffix, "")
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Pydantic input models
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ class _PositionInput(BaseModel):
38
+ file: str
39
+ line: int
40
+ column: int
41
+
42
+
43
+ class _FileInput(BaseModel):
44
+ file: str
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Tools
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ class LspGotoDefinitionTool(Tool):
53
+ """Jump to the definition of a symbol at a given file position."""
54
+
55
+ def __init__(self, manager: LspServerManager) -> None:
56
+ self._manager = manager
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return "lsp_goto_definition"
61
+
62
+ @property
63
+ def description(self) -> str:
64
+ return (
65
+ "Go to the definition of the symbol at the given file position "
66
+ "using the language server."
67
+ )
68
+
69
+ @property
70
+ def input_schema(self) -> dict:
71
+ return {
72
+ "type": "object",
73
+ "properties": {
74
+ "file": {"type": "string", "description": "Absolute path to the file"},
75
+ "line": {
76
+ "type": "integer",
77
+ "description": "0-based line number",
78
+ },
79
+ "column": {
80
+ "type": "integer",
81
+ "description": "0-based column number",
82
+ },
83
+ },
84
+ "required": ["file", "line", "column"],
85
+ }
86
+
87
+ @property
88
+ def required_permission(self) -> PermissionLevel:
89
+ return PermissionLevel.READ_ONLY
90
+
91
+ @property
92
+ def input_model(self) -> type[_PositionInput]:
93
+ return _PositionInput
94
+
95
+ def is_read_only(self, args: dict) -> bool:
96
+ return True
97
+
98
+ def is_concurrency_safe(self, args: dict) -> bool:
99
+ return True
100
+
101
+ def execute(self, args: dict) -> ToolResult:
102
+ import asyncio
103
+ import concurrent.futures
104
+ try:
105
+ loop = asyncio.get_running_loop()
106
+ except RuntimeError:
107
+ loop = None
108
+ if loop and loop.is_running():
109
+ with concurrent.futures.ThreadPoolExecutor() as pool:
110
+ return pool.submit(asyncio.run, self.execute_async(args)).result()
111
+ return asyncio.run(self.execute_async(args))
112
+
113
+ async def execute_async(self, args: dict) -> ToolResult:
114
+ file_path = args["file"]
115
+ line = int(args["line"])
116
+ column = int(args["column"])
117
+
118
+ language = _language_for_file(file_path)
119
+ client = self._manager.get_client(language)
120
+ if client is None:
121
+ return ToolResult(
122
+ output=f"No LSP client available for language '{language}' (file: {file_path})",
123
+ is_error=True,
124
+ )
125
+
126
+ file_uri = Path(file_path).as_uri()
127
+ locations = await client.goto_definition(file_uri, line, column)
128
+
129
+ if not locations:
130
+ return ToolResult(output="No definition found.")
131
+
132
+ lines = []
133
+ for loc in locations:
134
+ lines.append(f"{loc.file}:{loc.line}:{loc.column}")
135
+ return ToolResult(output="\n".join(lines))
136
+
137
+
138
+ class LspFindReferencesTool(Tool):
139
+ """Find all references to the symbol at a given file position."""
140
+
141
+ def __init__(self, manager: LspServerManager) -> None:
142
+ self._manager = manager
143
+
144
+ @property
145
+ def name(self) -> str:
146
+ return "lsp_find_references"
147
+
148
+ @property
149
+ def description(self) -> str:
150
+ return (
151
+ "Find all references to the symbol at the given file position "
152
+ "using the language server."
153
+ )
154
+
155
+ @property
156
+ def input_schema(self) -> dict:
157
+ return {
158
+ "type": "object",
159
+ "properties": {
160
+ "file": {"type": "string", "description": "Absolute path to the file"},
161
+ "line": {"type": "integer", "description": "0-based line number"},
162
+ "column": {"type": "integer", "description": "0-based column number"},
163
+ },
164
+ "required": ["file", "line", "column"],
165
+ }
166
+
167
+ @property
168
+ def required_permission(self) -> PermissionLevel:
169
+ return PermissionLevel.READ_ONLY
170
+
171
+ @property
172
+ def input_model(self) -> type[_PositionInput]:
173
+ return _PositionInput
174
+
175
+ def is_read_only(self, args: dict) -> bool:
176
+ return True
177
+
178
+ def is_concurrency_safe(self, args: dict) -> bool:
179
+ return True
180
+
181
+ def execute(self, args: dict) -> ToolResult:
182
+ import asyncio
183
+ import concurrent.futures
184
+ try:
185
+ loop = asyncio.get_running_loop()
186
+ except RuntimeError:
187
+ loop = None
188
+ if loop and loop.is_running():
189
+ with concurrent.futures.ThreadPoolExecutor() as pool:
190
+ return pool.submit(asyncio.run, self.execute_async(args)).result()
191
+ return asyncio.run(self.execute_async(args))
192
+
193
+ async def execute_async(self, args: dict) -> ToolResult:
194
+ file_path = args["file"]
195
+ line = int(args["line"])
196
+ column = int(args["column"])
197
+
198
+ language = _language_for_file(file_path)
199
+ client = self._manager.get_client(language)
200
+ if client is None:
201
+ return ToolResult(
202
+ output=f"No LSP client available for language '{language}' (file: {file_path})",
203
+ is_error=True,
204
+ )
205
+
206
+ file_uri = Path(file_path).as_uri()
207
+ references = await client.find_references(file_uri, line, column)
208
+
209
+ if not references:
210
+ return ToolResult(output="No references found.")
211
+
212
+ lines = []
213
+ for loc in references:
214
+ lines.append(f"{loc.file}:{loc.line}:{loc.column}")
215
+ return ToolResult(output="\n".join(lines))
216
+
217
+
218
+ class LspDiagnosticsTool(Tool):
219
+ """Get diagnostics (errors, warnings) for a file from the language server."""
220
+
221
+ def __init__(self, manager: LspServerManager) -> None:
222
+ self._manager = manager
223
+
224
+ @property
225
+ def name(self) -> str:
226
+ return "lsp_diagnostics"
227
+
228
+ @property
229
+ def description(self) -> str:
230
+ return "Get diagnostics (errors, warnings, hints) for a file using the language server."
231
+
232
+ @property
233
+ def input_schema(self) -> dict:
234
+ return {
235
+ "type": "object",
236
+ "properties": {
237
+ "file": {"type": "string", "description": "Absolute path to the file"},
238
+ },
239
+ "required": ["file"],
240
+ }
241
+
242
+ @property
243
+ def required_permission(self) -> PermissionLevel:
244
+ return PermissionLevel.READ_ONLY
245
+
246
+ @property
247
+ def input_model(self) -> type[_FileInput]:
248
+ return _FileInput
249
+
250
+ def is_read_only(self, args: dict) -> bool:
251
+ return True
252
+
253
+ def is_concurrency_safe(self, args: dict) -> bool:
254
+ return True
255
+
256
+ def execute(self, args: dict) -> ToolResult:
257
+ import asyncio
258
+ import concurrent.futures
259
+ try:
260
+ loop = asyncio.get_running_loop()
261
+ except RuntimeError:
262
+ loop = None
263
+ if loop and loop.is_running():
264
+ with concurrent.futures.ThreadPoolExecutor() as pool:
265
+ return pool.submit(asyncio.run, self.execute_async(args)).result()
266
+ return asyncio.run(self.execute_async(args))
267
+
268
+ async def execute_async(self, args: dict) -> ToolResult:
269
+ file_path = args["file"]
270
+
271
+ language = _language_for_file(file_path)
272
+ client = self._manager.get_client(language)
273
+ if client is None:
274
+ return ToolResult(
275
+ output=f"No LSP client available for language '{language}' (file: {file_path})",
276
+ is_error=True,
277
+ )
278
+
279
+ file_uri = Path(file_path).as_uri()
280
+ diagnostics = await client.get_diagnostics(file_uri)
281
+
282
+ if not diagnostics:
283
+ return ToolResult(output="No diagnostics found — file looks clean.")
284
+
285
+ lines = []
286
+ for d in diagnostics:
287
+ lines.append(f"{d.file}:{d.line}:{d.column} [{d.severity}] {d.message} ({d.source})")
288
+ return ToolResult(output="\n".join(lines))
File without changes