crabcode 0.1.6__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,332 @@
1
+ """Debug adapter discovery and launch configuration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from crabcode_core.subprocess_utils import (
12
+ resolve_executable_command,
13
+ subprocess_group_options,
14
+ )
15
+
16
+
17
+ _EXTENSION_LANGUAGE_MAP: dict[str, str] = {
18
+ ".c": "cpp",
19
+ ".cc": "cpp",
20
+ ".cpp": "cpp",
21
+ ".cxx": "cpp",
22
+ ".h": "cpp",
23
+ ".hh": "cpp",
24
+ ".hpp": "cpp",
25
+ ".hxx": "cpp",
26
+ ".rs": "rust",
27
+ ".py": "python",
28
+ ".pyi": "python",
29
+ ".go": "go",
30
+ ".java": "java",
31
+ ".js": "javascript",
32
+ ".jsx": "javascript",
33
+ ".mjs": "javascript",
34
+ ".cjs": "javascript",
35
+ ".ts": "typescript",
36
+ ".tsx": "typescript",
37
+ }
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class AdapterCandidate:
42
+ """A DAP adapter candidate for a language."""
43
+
44
+ language: str
45
+ adapter_id: str
46
+ label: str
47
+ command: list[str]
48
+ install_hint: str
49
+ official: bool = True
50
+ probe_command: list[str] | None = None
51
+
52
+
53
+ @dataclass
54
+ class AdapterStatus:
55
+ """Availability status for an adapter candidate."""
56
+
57
+ language: str
58
+ adapter_id: str
59
+ label: str
60
+ command: list[str]
61
+ available: bool
62
+ reason: str
63
+ install_hint: str
64
+ official: bool = True
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ return {
68
+ "language": self.language,
69
+ "adapter_id": self.adapter_id,
70
+ "label": self.label,
71
+ "command": self.command,
72
+ "available": self.available,
73
+ "reason": self.reason,
74
+ "install_hint": self.install_hint,
75
+ "official": self.official,
76
+ }
77
+
78
+
79
+ def infer_language(path_or_language: str | None) -> str | None:
80
+ """Infer a normalized language name from a language or file path."""
81
+ if not path_or_language:
82
+ return None
83
+ value = path_or_language.strip().lower()
84
+ aliases = {
85
+ "c": "cpp",
86
+ "c++": "cpp",
87
+ "cc": "cpp",
88
+ "cxx": "cpp",
89
+ "py": "python",
90
+ "js": "javascript",
91
+ "node": "javascript",
92
+ "ts": "typescript",
93
+ }
94
+ if value in aliases:
95
+ return aliases[value]
96
+ if value in {
97
+ "cpp",
98
+ "rust",
99
+ "python",
100
+ "go",
101
+ "java",
102
+ "typescript",
103
+ "javascript",
104
+ }:
105
+ return value
106
+ suffix = Path(value).suffix.lower()
107
+ return _EXTENSION_LANGUAGE_MAP.get(suffix)
108
+
109
+
110
+ class AdapterRegistry:
111
+ """Finds configured and built-in DAP adapters."""
112
+
113
+ def __init__(self, config: dict[str, Any] | None = None) -> None:
114
+ self._config = config or {}
115
+ self._timeout = float(self._config.get("adapter_probe_timeout_seconds", 2))
116
+
117
+ def status(self, language: str | None = None) -> list[AdapterStatus]:
118
+ """Return adapter status for one language or all supported languages."""
119
+ candidates = self._configured_candidates() + self._builtin_candidates()
120
+ wanted = infer_language(language) if language else None
121
+ results: list[AdapterStatus] = []
122
+ seen: set[tuple[str, str, tuple[str, ...]]] = set()
123
+ for candidate in candidates:
124
+ if wanted and candidate.language != wanted:
125
+ continue
126
+ key = (candidate.language, candidate.adapter_id, tuple(candidate.command))
127
+ if key in seen:
128
+ continue
129
+ seen.add(key)
130
+ results.append(self._probe(candidate))
131
+ return results
132
+
133
+ def resolve(self, language: str, adapter_id: str | None = None) -> AdapterStatus | None:
134
+ """Return the preferred available adapter for a language."""
135
+ normalized = infer_language(language)
136
+ if not normalized:
137
+ return None
138
+ statuses = self.status(normalized)
139
+ available = [s for s in statuses if s.available]
140
+ if adapter_id:
141
+ for status in available:
142
+ if status.adapter_id == adapter_id:
143
+ return status
144
+ return None
145
+ return available[0] if available else None
146
+
147
+ def install_hints(self, language: str | None = None) -> dict[str, list[str]]:
148
+ hints: dict[str, list[str]] = {}
149
+ for status in self.status(language):
150
+ if not status.available:
151
+ hints.setdefault(status.language, [])
152
+ if status.install_hint not in hints[status.language]:
153
+ hints[status.language].append(status.install_hint)
154
+ return hints
155
+
156
+ def _configured_candidates(self) -> list[AdapterCandidate]:
157
+ adapters = self._config.get("adapters", {})
158
+ if not isinstance(adapters, dict):
159
+ return []
160
+ candidates: list[AdapterCandidate] = []
161
+ for language, raw in adapters.items():
162
+ normalized = infer_language(str(language))
163
+ if not normalized or not isinstance(raw, dict):
164
+ continue
165
+ command = raw.get("command")
166
+ if not isinstance(command, list) or not command:
167
+ continue
168
+ command = [str(part) for part in command]
169
+ candidates.append(
170
+ AdapterCandidate(
171
+ language=normalized,
172
+ adapter_id=str(raw.get("adapter_id", "configured")),
173
+ label=str(raw.get("label", f"Configured {normalized} adapter")),
174
+ command=command,
175
+ install_hint="Configured in tool_settings.Debugger.adapters.",
176
+ official=bool(raw.get("official", False)),
177
+ probe_command=[str(part) for part in raw.get("probe_command", [])]
178
+ if isinstance(raw.get("probe_command"), list)
179
+ else None,
180
+ )
181
+ )
182
+ return candidates
183
+
184
+ def _probe(self, candidate: AdapterCandidate) -> AdapterStatus:
185
+ executable = candidate.command[0]
186
+ if not shutil.which(executable):
187
+ return AdapterStatus(
188
+ language=candidate.language,
189
+ adapter_id=candidate.adapter_id,
190
+ label=candidate.label,
191
+ command=candidate.command,
192
+ available=False,
193
+ reason=f"executable not found: {executable}",
194
+ install_hint=candidate.install_hint,
195
+ official=candidate.official,
196
+ )
197
+ probe = candidate.probe_command
198
+ if probe:
199
+ try:
200
+ result = subprocess.run(
201
+ resolve_executable_command(probe),
202
+ stdout=subprocess.DEVNULL,
203
+ stderr=subprocess.DEVNULL,
204
+ timeout=self._timeout,
205
+ check=False,
206
+ **subprocess_group_options(),
207
+ )
208
+ except Exception as exc:
209
+ return AdapterStatus(
210
+ language=candidate.language,
211
+ adapter_id=candidate.adapter_id,
212
+ label=candidate.label,
213
+ command=candidate.command,
214
+ available=False,
215
+ reason=f"probe failed: {exc}",
216
+ install_hint=candidate.install_hint,
217
+ official=candidate.official,
218
+ )
219
+ if result.returncode != 0:
220
+ return AdapterStatus(
221
+ language=candidate.language,
222
+ adapter_id=candidate.adapter_id,
223
+ label=candidate.label,
224
+ command=candidate.command,
225
+ available=False,
226
+ reason=f"probe exited with {result.returncode}",
227
+ install_hint=candidate.install_hint,
228
+ official=candidate.official,
229
+ )
230
+ return AdapterStatus(
231
+ language=candidate.language,
232
+ adapter_id=candidate.adapter_id,
233
+ label=candidate.label,
234
+ command=candidate.command,
235
+ available=True,
236
+ reason="available",
237
+ install_hint=candidate.install_hint,
238
+ official=candidate.official,
239
+ )
240
+
241
+ @staticmethod
242
+ def _builtin_candidates() -> list[AdapterCandidate]:
243
+ return [
244
+ AdapterCandidate(
245
+ language="python",
246
+ adapter_id="debugpy",
247
+ label="debugpy",
248
+ command=["python", "-m", "debugpy.adapter"],
249
+ probe_command=["python", "-c", "import debugpy.adapter"],
250
+ install_hint="Install Python debugpy: python -m pip install debugpy",
251
+ ),
252
+ AdapterCandidate(
253
+ language="python",
254
+ adapter_id="debugpy-python3",
255
+ label="debugpy via python3",
256
+ command=["python3", "-m", "debugpy.adapter"],
257
+ probe_command=["python3", "-c", "import debugpy.adapter"],
258
+ install_hint="Install Python debugpy: python3 -m pip install debugpy",
259
+ ),
260
+ AdapterCandidate(
261
+ language="go",
262
+ adapter_id="delve",
263
+ label="Delve DAP",
264
+ command=["dlv", "dap"],
265
+ install_hint="Install Delve: go install github.com/go-delve/delve/cmd/dlv@latest",
266
+ ),
267
+ AdapterCandidate(
268
+ language="cpp",
269
+ adapter_id="lldb-dap",
270
+ label="LLVM lldb-dap",
271
+ command=["lldb-dap"],
272
+ install_hint="Install LLVM lldb-dap from your LLVM/LLDB toolchain.",
273
+ ),
274
+ AdapterCandidate(
275
+ language="cpp",
276
+ adapter_id="lldb-vscode",
277
+ label="LLVM lldb-vscode",
278
+ command=["lldb-vscode"],
279
+ install_hint="Install LLVM/LLDB; older distributions expose lldb-vscode.",
280
+ ),
281
+ AdapterCandidate(
282
+ language="cpp",
283
+ adapter_id="gdb-dap",
284
+ label="GDB DAP",
285
+ command=["gdb", "--interpreter=dap"],
286
+ install_hint="Install a GDB build with DAP support.",
287
+ ),
288
+ AdapterCandidate(
289
+ language="rust",
290
+ adapter_id="lldb-dap",
291
+ label="LLVM lldb-dap",
292
+ command=["lldb-dap"],
293
+ install_hint="Install LLVM lldb-dap and build Rust binaries with debug symbols.",
294
+ ),
295
+ AdapterCandidate(
296
+ language="rust",
297
+ adapter_id="lldb-vscode",
298
+ label="LLVM lldb-vscode",
299
+ command=["lldb-vscode"],
300
+ install_hint="Install LLVM/LLDB and build Rust binaries with debug symbols.",
301
+ ),
302
+ AdapterCandidate(
303
+ language="java",
304
+ adapter_id="vscode-java-debug",
305
+ label="Java Debug Server",
306
+ command=["java-debug-adapter"],
307
+ install_hint=(
308
+ "Install the official vscode-java-debug adapter and configure "
309
+ "tool_settings.Debugger.adapters.java.command if it is not on PATH."
310
+ ),
311
+ ),
312
+ AdapterCandidate(
313
+ language="javascript",
314
+ adapter_id="vscode-js-debug",
315
+ label="Microsoft vscode-js-debug",
316
+ command=["js-debug-adapter"],
317
+ install_hint=(
318
+ "Install Microsoft vscode-js-debug and configure "
319
+ "tool_settings.Debugger.adapters.javascript.command if it is not on PATH."
320
+ ),
321
+ ),
322
+ AdapterCandidate(
323
+ language="typescript",
324
+ adapter_id="vscode-js-debug",
325
+ label="Microsoft vscode-js-debug",
326
+ command=["js-debug-adapter"],
327
+ install_hint=(
328
+ "Install Microsoft vscode-js-debug and configure "
329
+ "tool_settings.Debugger.adapters.typescript.command if it is not on PATH."
330
+ ),
331
+ ),
332
+ ]
@@ -0,0 +1,304 @@
1
+ """Small async Debug Adapter Protocol client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from crabcode_core.subprocess_utils import (
13
+ resolve_executable_command,
14
+ managed_process_command,
15
+ subprocess_group_options,
16
+ terminate_process_tree,
17
+ )
18
+
19
+
20
+ class DAPError(Exception):
21
+ """Raised when a DAP request fails."""
22
+
23
+
24
+ @dataclass
25
+ class DAPEvent:
26
+ event: str
27
+ body: dict[str, Any]
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {"event": self.event, "body": self.body}
31
+
32
+
33
+ class DAPClient:
34
+ """Communicates with a debug adapter over stdio."""
35
+
36
+ def __init__(
37
+ self,
38
+ command: list[str],
39
+ *,
40
+ cwd: str,
41
+ env: dict[str, str] | None = None,
42
+ timeout: float = 30.0,
43
+ max_events: int = 10_000,
44
+ ) -> None:
45
+ self.command = command
46
+ self.cwd = cwd
47
+ self.env = {**os.environ, **(env or {})}
48
+ if os.name == "nt":
49
+ self.env = {key.upper(): value for key, value in self.env.items()}
50
+ self.timeout = timeout
51
+ self._process: asyncio.subprocess.Process | None = None
52
+ self._reader_task: asyncio.Task[None] | None = None
53
+ self._stderr_task: asyncio.Task[None] | None = None
54
+ self._seq = 0
55
+ self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
56
+ self._pending_commands: dict[int, str] = {}
57
+ self._write_lock = asyncio.Lock()
58
+ self.events: asyncio.Queue[DAPEvent] = asyncio.Queue(maxsize=max(1, int(max_events)))
59
+ self._dropped_events = 0
60
+
61
+ async def start(self) -> None:
62
+ if self._process is not None:
63
+ if self._process.returncode is None:
64
+ return
65
+ raise DAPError(f"debug adapter exited with code {self._process.returncode}")
66
+ launch_command = resolve_executable_command(self.command, env=self.env, cwd=self.cwd)
67
+ self._process = await asyncio.create_subprocess_exec(
68
+ *managed_process_command(launch_command),
69
+ cwd=self.cwd,
70
+ env=self.env,
71
+ stdin=asyncio.subprocess.PIPE,
72
+ stdout=asyncio.subprocess.PIPE,
73
+ stderr=asyncio.subprocess.PIPE,
74
+ **subprocess_group_options(),
75
+ )
76
+ self._reader_task = asyncio.create_task(self._reader_loop())
77
+ self._stderr_task = asyncio.create_task(self._drain_stderr())
78
+
79
+ async def close(self) -> None:
80
+ for future in list(self._pending.values()):
81
+ if not future.done():
82
+ future.cancel()
83
+ elif not future.cancelled():
84
+ future.exception()
85
+ self._pending.clear()
86
+ self._pending_commands.clear()
87
+
88
+ if self._reader_task and not self._reader_task.done():
89
+ self._reader_task.cancel()
90
+ try:
91
+ await self._reader_task
92
+ except asyncio.CancelledError:
93
+ pass
94
+ self._reader_task = None
95
+
96
+ if self._stderr_task and not self._stderr_task.done():
97
+ self._stderr_task.cancel()
98
+ try:
99
+ await self._stderr_task
100
+ except asyncio.CancelledError:
101
+ pass
102
+ self._stderr_task = None
103
+
104
+ if self._process and self._process.returncode is None:
105
+ await terminate_process_tree(self._process)
106
+ self._process = None
107
+
108
+ async def request(
109
+ self,
110
+ command: str,
111
+ arguments: dict[str, Any] | None = None,
112
+ *,
113
+ timeout: float | None = None,
114
+ ) -> dict[str, Any]:
115
+ seq = await self.start_request(command, arguments)
116
+ return await self.wait_response(seq, timeout=timeout)
117
+
118
+ async def start_request(
119
+ self,
120
+ command: str,
121
+ arguments: dict[str, Any] | None = None,
122
+ ) -> int:
123
+ """Send a DAP request and return its sequence without waiting.
124
+
125
+ Some DAP requests, notably launch/attach, may not respond until after
126
+ the client sends configurationDone. The session manager uses this to
127
+ keep those requests pending while breakpoints are configured.
128
+ """
129
+ await self.start()
130
+ if not self._process or not self._process.stdin:
131
+ raise DAPError("debug adapter process is not running")
132
+
133
+ self._seq += 1
134
+ seq = self._seq
135
+ payload = {
136
+ "seq": seq,
137
+ "type": "request",
138
+ "command": command,
139
+ "arguments": arguments or {},
140
+ }
141
+ loop = asyncio.get_running_loop()
142
+ future: asyncio.Future[dict[str, Any]] = loop.create_future()
143
+ self._pending[seq] = future
144
+ self._pending_commands[seq] = command
145
+ await self._send(payload)
146
+ return seq
147
+
148
+ async def wait_response(
149
+ self,
150
+ seq: int,
151
+ *,
152
+ timeout: float | None = None,
153
+ ) -> dict[str, Any]:
154
+ future = self._pending.get(seq)
155
+ if future is None:
156
+ raise DAPError(f"unknown pending DAP request: {seq}")
157
+ command = self._pending_commands.get(seq, f"seq {seq}")
158
+ try:
159
+ response = await asyncio.wait_for(future, timeout=timeout or self.timeout)
160
+ finally:
161
+ self._pending.pop(seq, None)
162
+ self._pending_commands.pop(seq, None)
163
+ if not response.get("success", False):
164
+ error = response.get("body", {}).get("error", {})
165
+ message = response.get("message") or error.get("format")
166
+ raise DAPError(str(message or f"DAP request failed: {command}"))
167
+ body = response.get("body", {})
168
+ return body if isinstance(body, dict) else {"value": body}
169
+
170
+ async def drain_events(self) -> list[DAPEvent]:
171
+ drained: list[DAPEvent] = []
172
+ if self._dropped_events:
173
+ drained.append(
174
+ DAPEvent(
175
+ event="output",
176
+ body={
177
+ "category": "console",
178
+ "output": f"CrabCode dropped {self._dropped_events} buffered DAP events.\n",
179
+ },
180
+ )
181
+ )
182
+ self._dropped_events = 0
183
+ while True:
184
+ try:
185
+ drained.append(self.events.get_nowait())
186
+ except asyncio.QueueEmpty:
187
+ break
188
+ return drained
189
+
190
+ async def wait_for_event(
191
+ self,
192
+ event_name: str,
193
+ *,
194
+ timeout: float | None = None,
195
+ ) -> DAPEvent | None:
196
+ deadline = time.monotonic() + (timeout if timeout is not None else self.timeout)
197
+ unmatched: list[DAPEvent] = []
198
+ try:
199
+ while True:
200
+ remaining = deadline - time.monotonic()
201
+ if remaining <= 0:
202
+ return None
203
+ event = await asyncio.wait_for(self.events.get(), timeout=remaining)
204
+ if event.event == event_name:
205
+ return event
206
+ unmatched.append(event)
207
+ except asyncio.TimeoutError:
208
+ return None
209
+ finally:
210
+ for event in unmatched:
211
+ self._queue_event(event)
212
+
213
+ async def _send(self, payload: dict[str, Any]) -> None:
214
+ if not self._process or not self._process.stdin:
215
+ raise DAPError("debug adapter process is not running")
216
+ body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
217
+ header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
218
+ async with self._write_lock:
219
+ self._process.stdin.write(header + body)
220
+ await self._process.stdin.drain()
221
+
222
+ async def _reader_loop(self) -> None:
223
+ if not self._process or not self._process.stdout:
224
+ return
225
+ reader = self._process.stdout
226
+ while True:
227
+ try:
228
+ headers: dict[str, str] = {}
229
+ while True:
230
+ line = await reader.readline()
231
+ if not line:
232
+ raise EOFError("debug adapter closed stdout")
233
+ if line in {b"\r\n", b"\n"}:
234
+ break
235
+ text = line.decode("ascii", errors="replace").strip()
236
+ if ":" in text:
237
+ key, value = text.split(":", 1)
238
+ headers[key.lower()] = value.strip()
239
+ length_raw = headers.get("content-length")
240
+ if not length_raw:
241
+ continue
242
+ body = await reader.readexactly(int(length_raw))
243
+ message = json.loads(body.decode("utf-8", errors="replace"))
244
+ except asyncio.CancelledError:
245
+ raise
246
+ except Exception as exc:
247
+ for future in list(self._pending.values()):
248
+ if not future.done():
249
+ future.set_exception(DAPError(f"DAP reader failed: {exc}"))
250
+ return
251
+ await self._handle_message(message)
252
+
253
+ async def _handle_message(self, message: dict[str, Any]) -> None:
254
+ msg_type = message.get("type")
255
+ if msg_type == "response":
256
+ request_seq = message.get("request_seq")
257
+ future = self._pending.get(request_seq)
258
+ if future and not future.done():
259
+ future.set_result(message)
260
+ return
261
+ if msg_type == "event":
262
+ event = str(message.get("event", ""))
263
+ body = message.get("body", {})
264
+ event_body = body if isinstance(body, dict) else {}
265
+ self._queue_event(DAPEvent(event=event, body=event_body))
266
+
267
+ def _queue_event(self, event: DAPEvent) -> None:
268
+ try:
269
+ self.events.put_nowait(event)
270
+ return
271
+ except asyncio.QueueFull:
272
+ pass
273
+
274
+ if event.event == "output":
275
+ self._dropped_events += 1
276
+ return
277
+
278
+ retained: list[DAPEvent] = []
279
+ discarded = False
280
+ while True:
281
+ try:
282
+ queued = self.events.get_nowait()
283
+ except asyncio.QueueEmpty:
284
+ break
285
+ if not discarded and queued.event == "output":
286
+ self._dropped_events += 1
287
+ discarded = True
288
+ continue
289
+ retained.append(queued)
290
+
291
+ if not discarded and retained:
292
+ retained.pop(0)
293
+ self._dropped_events += 1
294
+ for queued in retained:
295
+ self.events.put_nowait(queued)
296
+ self.events.put_nowait(event)
297
+
298
+ async def _drain_stderr(self) -> None:
299
+ if not self._process or not self._process.stderr:
300
+ return
301
+ while True:
302
+ line = await self._process.stderr.readline()
303
+ if not line:
304
+ return