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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
llm_code/lsp/detector.py
ADDED
|
@@ -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
|
llm_code/lsp/manager.py
ADDED
|
@@ -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
|