local-coding-agent 1.1.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.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""LspManager: High-Level Language Server Navigation & Dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Any, Mapping, Sequence
|
|
10
|
+
|
|
11
|
+
from ._client import LspClient
|
|
12
|
+
from ._fallback import FallbackLspEngine
|
|
13
|
+
from ._types import (
|
|
14
|
+
DEFAULT_SERVER_CANDIDATES,
|
|
15
|
+
EXTENSION_TO_LANGUAGE,
|
|
16
|
+
LspHoverResult,
|
|
17
|
+
LspLocation,
|
|
18
|
+
LspSymbol,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LspManager:
|
|
23
|
+
"""High-level LSP manager routing queries to active language servers or fallback engine."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
workspace_root: str | Path | None = None,
|
|
28
|
+
*,
|
|
29
|
+
use_fallback_if_missing: bool = True,
|
|
30
|
+
server_candidates: Mapping[str, Sequence[Sequence[str]]] | None = None,
|
|
31
|
+
timeout: float = 10.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.workspace_root = str(Path(workspace_root).resolve()) if workspace_root else os.getcwd()
|
|
34
|
+
self.use_fallback_if_missing = use_fallback_if_missing
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.candidates: dict[str, list[list[str]]] = {
|
|
37
|
+
k: [list(cmd) for cmd in v]
|
|
38
|
+
for k, v in (server_candidates or DEFAULT_SERVER_CANDIDATES).items()
|
|
39
|
+
}
|
|
40
|
+
self._clients: dict[str, LspClient] = {}
|
|
41
|
+
self._fallback_engine = FallbackLspEngine()
|
|
42
|
+
self._lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
def get_language_id(self, file_path: str | Path) -> str:
|
|
45
|
+
"""Map file extension to standard LSP language ID."""
|
|
46
|
+
suffix = Path(file_path).suffix.lower()
|
|
47
|
+
return EXTENSION_TO_LANGUAGE.get(suffix, "plaintext")
|
|
48
|
+
|
|
49
|
+
def get_client(self, language_id: str) -> LspClient | None:
|
|
50
|
+
"""Get or spawn an active LspClient for the given language ID, or None if unavailable."""
|
|
51
|
+
with self._lock:
|
|
52
|
+
if language_id in self._clients:
|
|
53
|
+
client = self._clients[language_id]
|
|
54
|
+
if client.process is not None and client.process.poll() is None:
|
|
55
|
+
return client
|
|
56
|
+
# Stale process
|
|
57
|
+
client.stop()
|
|
58
|
+
del self._clients[language_id]
|
|
59
|
+
|
|
60
|
+
commands = self.candidates.get(language_id, [])
|
|
61
|
+
for cmd in commands:
|
|
62
|
+
binary = cmd[0]
|
|
63
|
+
if shutil.which(binary) is not None:
|
|
64
|
+
try:
|
|
65
|
+
client = LspClient(cmd, workspace_root=self.workspace_root, timeout=self.timeout)
|
|
66
|
+
client.start()
|
|
67
|
+
client.initialize(self.workspace_root)
|
|
68
|
+
self._clients[language_id] = client
|
|
69
|
+
return client
|
|
70
|
+
except Exception:
|
|
71
|
+
continue
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def go_to_definition(
|
|
75
|
+
self,
|
|
76
|
+
file_path: str | Path,
|
|
77
|
+
line: int,
|
|
78
|
+
character: int,
|
|
79
|
+
workspace_root: str | Path | None = None,
|
|
80
|
+
) -> list[LspLocation]:
|
|
81
|
+
"""Find definitions of symbol at (line, character) using LSP client or fallback."""
|
|
82
|
+
root = workspace_root or self.workspace_root
|
|
83
|
+
lang = self.get_language_id(file_path)
|
|
84
|
+
client = self.get_client(lang)
|
|
85
|
+
if client is not None:
|
|
86
|
+
try:
|
|
87
|
+
client.did_open(file_path)
|
|
88
|
+
return client.definition(file_path, line, character)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
if self.use_fallback_if_missing:
|
|
92
|
+
return self._fallback_engine.go_to_definition(file_path, line, character, root)
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
def find_references(
|
|
96
|
+
self,
|
|
97
|
+
file_path: str | Path,
|
|
98
|
+
line: int,
|
|
99
|
+
character: int,
|
|
100
|
+
workspace_root: str | Path | None = None,
|
|
101
|
+
include_declaration: bool = True,
|
|
102
|
+
) -> list[LspLocation]:
|
|
103
|
+
"""Find references of symbol at (line, character) using LSP client or fallback."""
|
|
104
|
+
root = workspace_root or self.workspace_root
|
|
105
|
+
lang = self.get_language_id(file_path)
|
|
106
|
+
client = self.get_client(lang)
|
|
107
|
+
if client is not None:
|
|
108
|
+
try:
|
|
109
|
+
client.did_open(file_path)
|
|
110
|
+
return client.references(file_path, line, character, include_declaration)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
if self.use_fallback_if_missing:
|
|
114
|
+
return self._fallback_engine.find_references(
|
|
115
|
+
file_path, line, character, root, include_declaration
|
|
116
|
+
)
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
def hover(
|
|
120
|
+
self,
|
|
121
|
+
file_path: str | Path,
|
|
122
|
+
line: int,
|
|
123
|
+
character: int,
|
|
124
|
+
workspace_root: str | Path | None = None,
|
|
125
|
+
) -> LspHoverResult | None:
|
|
126
|
+
"""Hover symbol at (line, character) using LSP client or fallback."""
|
|
127
|
+
root = workspace_root or self.workspace_root
|
|
128
|
+
lang = self.get_language_id(file_path)
|
|
129
|
+
client = self.get_client(lang)
|
|
130
|
+
if client is not None:
|
|
131
|
+
try:
|
|
132
|
+
client.did_open(file_path)
|
|
133
|
+
return client.hover(file_path, line, character)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
if self.use_fallback_if_missing:
|
|
137
|
+
return self._fallback_engine.hover(file_path, line, character, root)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def document_symbols(
|
|
141
|
+
self,
|
|
142
|
+
file_path: str | Path,
|
|
143
|
+
workspace_root: str | Path | None = None,
|
|
144
|
+
) -> list[LspSymbol]:
|
|
145
|
+
"""Extract symbol outline for file using LSP client or fallback."""
|
|
146
|
+
root = workspace_root or self.workspace_root
|
|
147
|
+
lang = self.get_language_id(file_path)
|
|
148
|
+
client = self.get_client(lang)
|
|
149
|
+
if client is not None:
|
|
150
|
+
try:
|
|
151
|
+
client.did_open(file_path)
|
|
152
|
+
return client.document_symbols(file_path)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
if self.use_fallback_if_missing:
|
|
156
|
+
return self._fallback_engine.document_symbols(file_path)
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
def close_all(self) -> None:
|
|
160
|
+
"""Shut down and stop all running language servers."""
|
|
161
|
+
with self._lock:
|
|
162
|
+
for client in self._clients.values():
|
|
163
|
+
try:
|
|
164
|
+
client.stop()
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
self._clients.clear()
|
|
168
|
+
|
|
169
|
+
def __enter__(self) -> LspManager:
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
173
|
+
self.close_all()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Mock Language Server for Testing & Verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, BinaryIO, Mapping
|
|
6
|
+
|
|
7
|
+
from ._types import MessageDecoder, encode_message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockLspServer:
|
|
11
|
+
"""Mock LSP server implementing standard JSON-RPC over stdio streams."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.documents: dict[str, str] = {}
|
|
15
|
+
self.shutdown_received = False
|
|
16
|
+
|
|
17
|
+
def handle_message(self, message: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
18
|
+
"""Process one incoming message and return an optional response dict."""
|
|
19
|
+
msg_id = message.get("id")
|
|
20
|
+
method = message.get("method")
|
|
21
|
+
params = message.get("params") or {}
|
|
22
|
+
|
|
23
|
+
if method == "initialize":
|
|
24
|
+
return {
|
|
25
|
+
"jsonrpc": "2.0",
|
|
26
|
+
"id": msg_id,
|
|
27
|
+
"result": {
|
|
28
|
+
"capabilities": {
|
|
29
|
+
"definitionProvider": True,
|
|
30
|
+
"referencesProvider": True,
|
|
31
|
+
"hoverProvider": True,
|
|
32
|
+
"documentSymbolProvider": True,
|
|
33
|
+
"textDocumentSync": 1,
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if method == "initialized":
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
if method == "textDocument/didOpen":
|
|
42
|
+
doc = params.get("textDocument", {})
|
|
43
|
+
uri = doc.get("uri", "")
|
|
44
|
+
text = doc.get("text", "")
|
|
45
|
+
self.documents[uri] = text
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if method == "textDocument/didClose":
|
|
49
|
+
doc = params.get("textDocument", {})
|
|
50
|
+
uri = doc.get("uri", "")
|
|
51
|
+
self.documents.pop(uri, None)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if method == "textDocument/definition":
|
|
55
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
56
|
+
pos = params.get("position", {})
|
|
57
|
+
line = pos.get("line", 0)
|
|
58
|
+
char = pos.get("character", 0)
|
|
59
|
+
return {
|
|
60
|
+
"jsonrpc": "2.0",
|
|
61
|
+
"id": msg_id,
|
|
62
|
+
"result": [
|
|
63
|
+
{
|
|
64
|
+
"uri": uri,
|
|
65
|
+
"range": {
|
|
66
|
+
"start": {"line": line, "character": char},
|
|
67
|
+
"end": {"line": line, "character": char + 5},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if method == "textDocument/references":
|
|
74
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
75
|
+
pos = params.get("position", {})
|
|
76
|
+
line = pos.get("line", 0)
|
|
77
|
+
char = pos.get("character", 0)
|
|
78
|
+
return {
|
|
79
|
+
"jsonrpc": "2.0",
|
|
80
|
+
"id": msg_id,
|
|
81
|
+
"result": [
|
|
82
|
+
{
|
|
83
|
+
"uri": uri,
|
|
84
|
+
"range": {
|
|
85
|
+
"start": {"line": line, "character": char},
|
|
86
|
+
"end": {"line": line, "character": char + 5},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"uri": uri,
|
|
91
|
+
"range": {
|
|
92
|
+
"start": {"line": line + 2, "character": 0},
|
|
93
|
+
"end": {"line": line + 2, "character": 5},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if method == "textDocument/hover":
|
|
100
|
+
pos = params.get("position", {})
|
|
101
|
+
line = pos.get("line", 0)
|
|
102
|
+
char = pos.get("character", 0)
|
|
103
|
+
return {
|
|
104
|
+
"jsonrpc": "2.0",
|
|
105
|
+
"id": msg_id,
|
|
106
|
+
"result": {
|
|
107
|
+
"contents": {
|
|
108
|
+
"kind": "markdown",
|
|
109
|
+
"value": f"```python\ndef mock_symbol() -> None\n```\nMock documentation at L{line+1}:C{char+1}.",
|
|
110
|
+
},
|
|
111
|
+
"range": {
|
|
112
|
+
"start": {"line": line, "character": char},
|
|
113
|
+
"end": {"line": line, "character": char + 5},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if method == "textDocument/documentSymbol":
|
|
119
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
120
|
+
return {
|
|
121
|
+
"jsonrpc": "2.0",
|
|
122
|
+
"id": msg_id,
|
|
123
|
+
"result": [
|
|
124
|
+
{
|
|
125
|
+
"name": "MockClass",
|
|
126
|
+
"kind": 5,
|
|
127
|
+
"range": {
|
|
128
|
+
"start": {"line": 0, "character": 0},
|
|
129
|
+
"end": {"line": 10, "character": 0},
|
|
130
|
+
},
|
|
131
|
+
"selectionRange": {
|
|
132
|
+
"start": {"line": 0, "character": 6},
|
|
133
|
+
"end": {"line": 0, "character": 15},
|
|
134
|
+
},
|
|
135
|
+
"children": [
|
|
136
|
+
{
|
|
137
|
+
"name": "mock_method",
|
|
138
|
+
"kind": 6,
|
|
139
|
+
"range": {
|
|
140
|
+
"start": {"line": 1, "character": 4},
|
|
141
|
+
"end": {"line": 3, "character": 0},
|
|
142
|
+
},
|
|
143
|
+
"selectionRange": {
|
|
144
|
+
"start": {"line": 1, "character": 8},
|
|
145
|
+
"end": {"line": 1, "character": 19},
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "mock_function",
|
|
152
|
+
"kind": 12,
|
|
153
|
+
"range": {
|
|
154
|
+
"start": {"line": 12, "character": 0},
|
|
155
|
+
"end": {"line": 15, "character": 0},
|
|
156
|
+
},
|
|
157
|
+
"selectionRange": {
|
|
158
|
+
"start": {"line": 12, "character": 4},
|
|
159
|
+
"end": {"line": 12, "character": 17},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if method == "shutdown":
|
|
166
|
+
self.shutdown_received = True
|
|
167
|
+
return {"jsonrpc": "2.0", "id": msg_id, "result": None}
|
|
168
|
+
|
|
169
|
+
if method == "exit":
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Fallback for unknown methods
|
|
173
|
+
if msg_id is not None:
|
|
174
|
+
return {
|
|
175
|
+
"jsonrpc": "2.0",
|
|
176
|
+
"id": msg_id,
|
|
177
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
178
|
+
}
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def serve(self, input_stream: BinaryIO, output_stream: BinaryIO) -> None:
|
|
182
|
+
"""Run standard stdio server loop reading from input_stream and writing to output_stream."""
|
|
183
|
+
decoder = MessageDecoder()
|
|
184
|
+
read_fn = getattr(input_stream, "read1", input_stream.read)
|
|
185
|
+
while True:
|
|
186
|
+
try:
|
|
187
|
+
chunk = read_fn(4096)
|
|
188
|
+
except Exception:
|
|
189
|
+
break
|
|
190
|
+
if not chunk:
|
|
191
|
+
break
|
|
192
|
+
messages = decoder.push(chunk)
|
|
193
|
+
for msg in messages:
|
|
194
|
+
resp = self.handle_message(msg)
|
|
195
|
+
if resp is not None:
|
|
196
|
+
data = encode_message(resp)
|
|
197
|
+
output_stream.write(data)
|
|
198
|
+
output_stream.flush()
|
|
199
|
+
if msg.get("method") == "exit":
|
|
200
|
+
return
|