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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,398 @@
1
+ """LSP wire enums, exceptions, data models, and protocol helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Mapping, Sequence
10
+ import urllib.parse
11
+ import urllib.request
12
+
13
+
14
+ # ============================================================================
15
+ # Constants & LSP Wire Enums
16
+ # ============================================================================
17
+
18
+ HEADER_SEPARATOR = b"\r\n\r\n"
19
+ MAX_HEADER_BYTES = 64 * 1024
20
+ MAX_MESSAGE_BYTES = 10 * 1024 * 1024
21
+
22
+ SYMBOL_KIND_NAMES: dict[int, str] = {
23
+ 1: "File",
24
+ 2: "Module",
25
+ 3: "Namespace",
26
+ 4: "Package",
27
+ 5: "Class",
28
+ 6: "Method",
29
+ 7: "Property",
30
+ 8: "Field",
31
+ 9: "Constructor",
32
+ 10: "Enum",
33
+ 11: "Interface",
34
+ 12: "Function",
35
+ 13: "Variable",
36
+ 14: "Constant",
37
+ 15: "String",
38
+ 16: "Number",
39
+ 17: "Boolean",
40
+ 18: "Array",
41
+ 19: "Object",
42
+ 20: "Key",
43
+ 21: "Null",
44
+ 22: "EnumMember",
45
+ 23: "Struct",
46
+ 24: "Event",
47
+ 25: "Operator",
48
+ 26: "TypeParameter",
49
+ }
50
+
51
+ EXTENSION_TO_LANGUAGE: dict[str, str] = {
52
+ ".py": "python",
53
+ ".pyi": "python",
54
+ ".ts": "typescript",
55
+ ".tsx": "typescript",
56
+ ".js": "javascript",
57
+ ".jsx": "javascript",
58
+ ".mjs": "javascript",
59
+ ".cjs": "javascript",
60
+ ".rs": "rust",
61
+ ".go": "go",
62
+ }
63
+
64
+ DEFAULT_SERVER_CANDIDATES: dict[str, list[list[str]]] = {
65
+ "python": [
66
+ ["pyright-langserver", "--stdio"],
67
+ ["pyright", "--stdio"],
68
+ ["basedpyright-langserver", "--stdio"],
69
+ ["pylsp"],
70
+ ],
71
+ "typescript": [
72
+ ["typescript-language-server", "--stdio"],
73
+ ],
74
+ "javascript": [
75
+ ["typescript-language-server", "--stdio"],
76
+ ],
77
+ "rust": [
78
+ ["rust-analyzer"],
79
+ ],
80
+ "go": [
81
+ ["gopls"],
82
+ ],
83
+ }
84
+
85
+
86
+ # ============================================================================
87
+ # Exceptions
88
+ # ============================================================================
89
+
90
+ class LspError(RuntimeError):
91
+ """Base exception for LSP errors."""
92
+
93
+
94
+ class LspTimeoutError(LspError):
95
+ """An LSP request timed out waiting for server response."""
96
+
97
+
98
+ class LspConnectionError(LspError):
99
+ """LSP server process failed to start or crashed."""
100
+
101
+
102
+ class LspResponseError(LspError):
103
+ """Server returned an LSP JSON-RPC error response."""
104
+
105
+ def __init__(self, code: int, message: str, data: Any = None) -> None:
106
+ super().__init__(f"LSP error {code}: {message}")
107
+ self.code = code
108
+ self.message = message
109
+ self.data = data
110
+
111
+
112
+ # ============================================================================
113
+ # Data Models
114
+ # ============================================================================
115
+
116
+ @dataclass(frozen=True)
117
+ class LspPosition:
118
+ """A zero-based line and character cursor coordinate."""
119
+
120
+ line: int
121
+ character: int
122
+
123
+ def to_dict(self) -> dict[str, int]:
124
+ return {"line": self.line, "character": self.character}
125
+
126
+ @classmethod
127
+ def from_dict(cls, data: Mapping[str, Any]) -> LspPosition:
128
+ return cls(line=int(data["line"]), character=int(data["character"]))
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class LspRange:
133
+ """A half-open range `[start, end)` within a document."""
134
+
135
+ start: LspPosition
136
+ end: LspPosition
137
+
138
+ def to_dict(self) -> dict[str, Any]:
139
+ return {"start": self.start.to_dict(), "end": self.end.to_dict()}
140
+
141
+ @classmethod
142
+ def from_dict(cls, data: Mapping[str, Any]) -> LspRange:
143
+ return cls(
144
+ start=LspPosition.from_dict(data["start"]),
145
+ end=LspPosition.from_dict(data["end"]),
146
+ )
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class LspLocation:
151
+ """A document URI, range within it, and local filesystem path."""
152
+
153
+ uri: str
154
+ range: LspRange
155
+ file_path: str
156
+
157
+ def to_dict(self) -> dict[str, Any]:
158
+ return {
159
+ "uri": self.uri,
160
+ "range": self.range.to_dict(),
161
+ "file_path": self.file_path,
162
+ }
163
+
164
+ @classmethod
165
+ def from_dict(cls, data: Mapping[str, Any]) -> LspLocation:
166
+ uri = str(data["uri"])
167
+ file_path = str(data.get("file_path") or uri_to_path(uri))
168
+ return cls(
169
+ uri=uri,
170
+ range=LspRange.from_dict(data["range"]),
171
+ file_path=file_path,
172
+ )
173
+
174
+ @classmethod
175
+ def from_wire(cls, data: Mapping[str, Any]) -> LspLocation:
176
+ # Handles Location (uri, range) and LocationLink (targetUri, targetSelectionRange/targetRange)
177
+ if "targetUri" in data:
178
+ uri = str(data["targetUri"])
179
+ raw_range = data.get("targetSelectionRange") or data.get("targetRange") or {}
180
+ range_obj = LspRange.from_dict(raw_range)
181
+ else:
182
+ uri = str(data["uri"])
183
+ range_obj = LspRange.from_dict(data["range"])
184
+ return cls(uri=uri, range=range_obj, file_path=uri_to_path(uri))
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class LspHoverResult:
189
+ """Hover documentation or signature information."""
190
+
191
+ contents: str
192
+ range: LspRange | None = None
193
+
194
+ def to_dict(self) -> dict[str, Any]:
195
+ return {
196
+ "contents": self.contents,
197
+ "range": self.range.to_dict() if self.range is not None else None,
198
+ }
199
+
200
+ @classmethod
201
+ def from_dict(cls, data: Mapping[str, Any]) -> LspHoverResult:
202
+ raw_range = data.get("range")
203
+ range_obj = LspRange.from_dict(raw_range) if raw_range is not None else None
204
+ return cls(contents=str(data.get("contents", "")), range=range_obj)
205
+
206
+ @classmethod
207
+ def from_wire(cls, data: Any) -> LspHoverResult | None:
208
+ if not data or not isinstance(data, Mapping):
209
+ return None
210
+ raw_contents = data.get("contents")
211
+ contents = _render_hover_contents(raw_contents)
212
+ if not contents.strip():
213
+ return None
214
+ raw_range = data.get("range")
215
+ range_obj = LspRange.from_dict(raw_range) if isinstance(raw_range, Mapping) else None
216
+ return cls(contents=contents, range=range_obj)
217
+
218
+
219
+ @dataclass(frozen=True)
220
+ class LspSymbol:
221
+ """A symbol outline entry (function, class, variable, etc.)."""
222
+
223
+ name: str
224
+ kind: int
225
+ kind_name: str
226
+ location: LspLocation
227
+
228
+ def to_dict(self) -> dict[str, Any]:
229
+ return {
230
+ "name": self.name,
231
+ "kind": self.kind,
232
+ "kind_name": self.kind_name,
233
+ "location": self.location.to_dict(),
234
+ }
235
+
236
+ @classmethod
237
+ def from_dict(cls, data: Mapping[str, Any]) -> LspSymbol:
238
+ kind = int(data.get("kind", 12))
239
+ kind_name = str(data.get("kind_name") or SYMBOL_KIND_NAMES.get(kind, f"Kind({kind})"))
240
+ return cls(
241
+ name=str(data["name"]),
242
+ kind=kind,
243
+ kind_name=kind_name,
244
+ location=LspLocation.from_dict(data["location"]),
245
+ )
246
+
247
+ @classmethod
248
+ def from_wire(cls, data: Any, file_uri: str = "") -> list[LspSymbol]:
249
+ if not data or not isinstance(data, Sequence):
250
+ return []
251
+ symbols: list[LspSymbol] = []
252
+ for item in data:
253
+ if not isinstance(item, Mapping):
254
+ continue
255
+ name = str(item.get("name", ""))
256
+ kind = int(item.get("kind", 12))
257
+ kind_name = SYMBOL_KIND_NAMES.get(kind, f"Kind({kind})")
258
+
259
+ # Hierarchical DocumentSymbol
260
+ if "range" in item or "selectionRange" in item:
261
+ raw_range = item.get("selectionRange") or item.get("range")
262
+ range_obj = LspRange.from_dict(raw_range) if isinstance(raw_range, Mapping) else LspRange(
263
+ LspPosition(0, 0), LspPosition(0, 0)
264
+ )
265
+ loc = LspLocation(uri=file_uri, range=range_obj, file_path=uri_to_path(file_uri))
266
+ symbols.append(cls(name=name, kind=kind, kind_name=kind_name, location=loc))
267
+ # Recurse into children
268
+ children = item.get("children")
269
+ if children and isinstance(children, Sequence):
270
+ symbols.extend(cls.from_wire(children, file_uri=file_uri))
271
+ # Flat SymbolInformation
272
+ elif "location" in item and isinstance(item["location"], Mapping):
273
+ loc = LspLocation.from_wire(item["location"])
274
+ symbols.append(cls(name=name, kind=kind, kind_name=kind_name, location=loc))
275
+ return symbols
276
+
277
+
278
+ # ============================================================================
279
+ # Protocol Helpers & URI Conversion
280
+ # ============================================================================
281
+
282
+ def path_to_uri(file_path: str | Path) -> str:
283
+ """Convert a filesystem path to a standard file:// URI."""
284
+ path = Path(file_path).resolve()
285
+ return path.as_uri()
286
+
287
+
288
+ def uri_to_path(uri: str) -> str:
289
+ """Convert a file:// URI to a normalized filesystem path."""
290
+ if not uri.startswith("file://"):
291
+ return uri
292
+ parsed = urllib.parse.urlparse(uri)
293
+ path = urllib.request.url2pathname(urllib.parse.unquote(parsed.path))
294
+ if os.name == "nt" and path.startswith("\\") and len(path) > 2 and path[2] == ":":
295
+ path = path[1:]
296
+ return os.path.normpath(path)
297
+
298
+
299
+ def _render_hover_contents(contents: Any) -> str:
300
+ """Normalize LSP hover contents into markdown text."""
301
+ if contents is None:
302
+ return ""
303
+ if isinstance(contents, str):
304
+ return contents
305
+ if isinstance(contents, Sequence) and not isinstance(contents, (bytes, bytearray)):
306
+ return "\n\n".join(_render_hover_contents(item) for item in contents if item)
307
+ if isinstance(contents, Mapping):
308
+ # MarkedString object: { language: string, value: string }
309
+ if "language" in contents and "value" in contents:
310
+ lang = contents.get("language", "")
311
+ val = contents.get("value", "")
312
+ return f"```{lang}\n{val}\n```"
313
+ # MarkupContent: { kind: 'markdown' | 'plaintext', value: string }
314
+ if "value" in contents and isinstance(contents["value"], str):
315
+ return contents["value"]
316
+ return str(contents)
317
+
318
+
319
+ # ============================================================================
320
+ # Base Protocol Framing (encode/decode)
321
+ # ============================================================================
322
+
323
+ def encode_message(message: dict[str, Any]) -> bytes:
324
+ """Encode a JSON-RPC message as a Content-Length framed byte sequence."""
325
+ body = json.dumps(message, ensure_ascii=False).encode("utf-8")
326
+ header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
327
+ return header + body
328
+
329
+
330
+ class MessageDecoder:
331
+ """Streaming decoder for Content-Length framed JSON-RPC messages."""
332
+
333
+ def __init__(
334
+ self,
335
+ max_message_bytes: int = MAX_MESSAGE_BYTES,
336
+ max_header_bytes: int = MAX_HEADER_BYTES,
337
+ ) -> None:
338
+ self.max_message_bytes = max_message_bytes
339
+ self.max_header_bytes = max_header_bytes
340
+ self._buffer = bytearray()
341
+
342
+ def push(self, chunk: bytes) -> list[dict[str, Any]]:
343
+ """Append raw incoming bytes and yield all newly completed JSON-RPC messages."""
344
+ self._buffer.extend(chunk)
345
+ messages: list[dict[str, Any]] = []
346
+ while True:
347
+ msg = self._next()
348
+ if msg is None:
349
+ break
350
+ messages.append(msg)
351
+ return messages
352
+
353
+ def _next(self) -> dict[str, Any] | None:
354
+ sep_idx = self._buffer.find(HEADER_SEPARATOR)
355
+ if sep_idx < 0:
356
+ if len(self._buffer) > self.max_header_bytes:
357
+ raise LspError(f"LSP header exceeded {self.max_header_bytes} bytes without terminator")
358
+ return None
359
+ if sep_idx > self.max_header_bytes:
360
+ raise LspError(f"LSP header exceeded {self.max_header_bytes} bytes")
361
+
362
+ header_text = self._buffer[:sep_idx].decode("ascii", errors="replace")
363
+ content_length = self._parse_content_length(header_text)
364
+ if content_length > self.max_message_bytes:
365
+ raise LspError(
366
+ f"LSP message length {content_length} exceeds limit {self.max_message_bytes}"
367
+ )
368
+
369
+ body_start = sep_idx + len(HEADER_SEPARATOR)
370
+ body_end = body_start + content_length
371
+ if len(self._buffer) < body_end:
372
+ return None
373
+
374
+ body_bytes = bytes(self._buffer[body_start:body_end])
375
+ del self._buffer[:body_end]
376
+
377
+ try:
378
+ return json.loads(body_bytes.decode("utf-8"))
379
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
380
+ raise LspError(f"LSP message body was not valid JSON: {e}") from e
381
+
382
+ @staticmethod
383
+ def _parse_content_length(header_text: str) -> int:
384
+ for line in header_text.split("\r\n"):
385
+ colon = line.find(":")
386
+ if colon < 0:
387
+ continue
388
+ name = line[:colon].strip().lower()
389
+ if name == "content-length":
390
+ val = line[colon + 1:].strip()
391
+ try:
392
+ length = int(val)
393
+ if length < 0:
394
+ raise ValueError
395
+ return length
396
+ except ValueError:
397
+ raise LspError(f"Invalid Content-Length header: {line!r}")
398
+ raise LspError(f"LSP header block missing Content-Length: {header_text!r}")