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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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
|
+
]
|
crabcode_debugger/dap.py
ADDED
|
@@ -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
|