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
crabcode_cli/banner.py ADDED
@@ -0,0 +1,39 @@
1
+ """CrabCode terminal banner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.text import Text
8
+
9
+ # Pixel crab with open claws, eye stalks, and three pairs of splayed legs.
10
+ _CRAB = (
11
+ " ▄ ▄ ▄ ▄\n"
12
+ " ▀█▄█▀ ▄ ▄ ▀█▄█▀\n"
13
+ " ▀█▄▄ █ █ ▄▄█▀\n"
14
+ " ▄▄█████████████▄▄\n"
15
+ " ▄█▀ █████████████ ▀█▄\n"
16
+ " ▀ ▄█▀▀█████████▀▀█▄ ▀\n"
17
+ " ▀▀ ▄█▀ ▀█▄ ▀▀"
18
+ )
19
+
20
+ # "CrabCode" in figlet slant font
21
+ _CRABCODE = r""" ______ __ ______ __
22
+ / ____/________ _/ /_ / ____/___ ____/ /__
23
+ / / / ___/ __ `/ __ \/ / / __ \/ __ / _ \
24
+ / /___/ / / /_/ / /_/ / /___/ /_/ / /_/ / __/
25
+ \____/_/ \__,_/_.___/\____/\____/\__,_/\___/ """
26
+
27
+
28
+ def print_banner(console: Console) -> None:
29
+ """Print the CrabCode banner with pixel crab art on the left."""
30
+ grid = Table.grid(padding=(0, 3))
31
+ grid.add_column(no_wrap=True)
32
+ grid.add_column(no_wrap=True, vertical="middle")
33
+ grid.add_row(
34
+ Text(_CRAB, style="bold red"),
35
+ Text(_CRABCODE, style="bold cyan"),
36
+ )
37
+ console.print()
38
+ console.print(grid)
39
+ console.print()
@@ -0,0 +1,286 @@
1
+ """CLI adapter for the shared native executor (no Desktop, Gateway or Node)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import sys
11
+ import time
12
+ from typing import Any
13
+ from uuid import uuid4
14
+
15
+ from crabcode_core.computer_use_mode import effective_computer_use_mode
16
+ from crabcode_core.logging_utils import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+ _MAX_RESPONSE = 64 * 1024 * 1024
20
+ _HEARTBEAT_SECONDS = 30
21
+
22
+
23
+ class ComputerUseError(RuntimeError):
24
+ pass
25
+
26
+
27
+ def state_directory() -> Path:
28
+ if sys.platform == "darwin":
29
+ base = Path.home() / "Library/Application Support"
30
+ elif sys.platform == "win32":
31
+ base = Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData/Local")
32
+ else:
33
+ base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share")
34
+ return base / "CrabCode/ComputerUse"
35
+
36
+
37
+ def bundled_executable(directory: Path) -> Path:
38
+ if sys.platform == "darwin":
39
+ app = directory if directory.suffix == ".app" else directory / "Crab Computer Use.app"
40
+ return app / "Contents/MacOS/crabcode-computer-use-host"
41
+ return directory / ("crabcode-computer-use-host.exe" if sys.platform == "win32" else "crabcode-computer-use-host")
42
+
43
+
44
+ class NativeClient:
45
+ def __init__(self, state_dir: Path | None = None) -> None:
46
+ self.state_dir = state_dir or state_directory()
47
+
48
+ @property
49
+ def installed(self) -> bool:
50
+ return bundled_executable(self.state_dir).is_file()
51
+
52
+ @property
53
+ def bundle(self) -> Path | None:
54
+ directory = Path(__file__).with_name("_native")
55
+ return directory if bundled_executable(directory).is_file() else None
56
+
57
+ async def start(self) -> None:
58
+ # Platform wheels carry the signed app/executable. The host installs it
59
+ # atomically in the per-user shared location and reuses the singleton.
60
+ executable = bundled_executable(self.bundle or self.state_dir)
61
+ if not executable.is_file():
62
+ raise ComputerUseError(
63
+ "当前安装包未包含 Computer Use 原生组件;请使用包含原生组件的 CrabCode 平台安装包。"
64
+ )
65
+ process = None
66
+ try:
67
+ process = await asyncio.create_subprocess_exec(
68
+ str(executable), "--install-and-start", "--state-dir", str(self.state_dir),
69
+ stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
70
+ )
71
+ _, stderr = await asyncio.wait_for(process.communicate(), 25)
72
+ if process.returncode:
73
+ raise ComputerUseError(stderr.decode("utf-8", errors="replace").strip() or "Computer Use 组件启动失败")
74
+ except (OSError, asyncio.TimeoutError) as exc:
75
+ raise ComputerUseError("Computer Use 组件启动失败或超时;请检查 crabcode computer-use status。") from exc
76
+ finally:
77
+ if process is not None and process.returncode is None:
78
+ process.kill()
79
+ await process.wait()
80
+
81
+ def endpoint(self) -> dict[str, Any]:
82
+ try:
83
+ endpoint = json.loads((self.state_dir / "endpoint.json").read_text(encoding="utf-8"))
84
+ except (OSError, ValueError) as exc:
85
+ raise ComputerUseError("Computer Use 执行器未连接;请运行 crabcode computer-use status。") from exc
86
+ if not (
87
+ isinstance(endpoint, dict) and endpoint.get("protocol_version") == 1
88
+ and type(endpoint.get("port")) is int and 0 < endpoint["port"] <= 65535
89
+ and isinstance(endpoint.get("token"), str) and len(endpoint["token"]) == 64
90
+ and isinstance(endpoint.get("instance_id"), str) and endpoint["instance_id"]
91
+ ):
92
+ raise ComputerUseError("Computer Use 组件版本不兼容,请更新组件。")
93
+ return endpoint
94
+
95
+ async def request(
96
+ self, method: str, params: dict[str, Any] | None = None,
97
+ *, instance_id: str | None = None, timeout: float = 95,
98
+ ) -> Any:
99
+ endpoint = self.endpoint()
100
+ if instance_id is not None and endpoint["instance_id"] != instance_id:
101
+ raise ComputerUseError("Computer Use 执行器已重启,请重新检测并观察窗口。")
102
+ message = json.dumps({
103
+ "token": endpoint["token"], "instance_id": endpoint["instance_id"],
104
+ "method": method, "params": params or {},
105
+ "deadline_ms": int((time.time() + min(85, timeout)) * 1000),
106
+ }, ensure_ascii=False).encode("utf-8") + b"\n"
107
+ if len(message) > 1024 * 1024:
108
+ raise ComputerUseError("Computer Use 请求超过大小限制")
109
+
110
+ async def exchange() -> Any:
111
+ reader, writer = await asyncio.open_connection("127.0.0.1", endpoint["port"], limit=_MAX_RESPONSE)
112
+ try:
113
+ writer.write(message)
114
+ await writer.drain()
115
+ # Requests are never replayed: a missing reply cannot establish
116
+ # whether native input was already delivered.
117
+ try:
118
+ line = await reader.readuntil(b"\n")
119
+ except asyncio.IncompleteReadError as exc:
120
+ raise ComputerUseError("Computer Use 响应不完整;请先观察结果,不要直接重复输入。") from exc
121
+ except asyncio.LimitOverrunError as exc:
122
+ raise ComputerUseError("Computer Use 响应超过大小限制") from exc
123
+ try:
124
+ response = json.loads(line)
125
+ except (ValueError, UnicodeError) as exc:
126
+ raise ComputerUseError("Computer Use 响应无效;请先观察结果。") from exc
127
+ if not isinstance(response, dict) or response.get("ok") is not True:
128
+ error = response.get("error") if isinstance(response, dict) else None
129
+ raise ComputerUseError(str(error or "Computer Use 执行失败"))
130
+ return response.get("result")
131
+ finally:
132
+ writer.close()
133
+ with contextlib.suppress(OSError):
134
+ await writer.wait_closed()
135
+
136
+ try:
137
+ return await asyncio.wait_for(exchange(), timeout)
138
+ except asyncio.TimeoutError as exc:
139
+ raise ComputerUseError("Computer Use 执行器响应超时;请先观察结果,不要直接重复输入。") from exc
140
+ except OSError as exc:
141
+ raise ComputerUseError("Computer Use 执行器连接中断;请重新检测并观察结果。") from exc
142
+
143
+
144
+ class LocalComputerUse:
145
+ """The CoreSession backend contract, using the same IPC as the GUI clients."""
146
+
147
+ def __init__(self, client: NativeClient | None = None) -> None:
148
+ self.client = client or NativeClient()
149
+ self.host_id = f"cli-{uuid4()}"
150
+ self.connection_id = str(uuid4())
151
+ self._caps: dict[str, Any] = {}
152
+ self._heartbeat: asyncio.Task[None] | None = None
153
+ self._closed = False
154
+
155
+ async def connect(self) -> None:
156
+ await self.refresh()
157
+ self._heartbeat = asyncio.create_task(self._keep_alive())
158
+
159
+ async def refresh(self) -> None:
160
+ await self.client.start()
161
+ caps = await self.client.request("capabilities", timeout=25)
162
+ if not isinstance(caps, dict) or not caps.get("instance_id") or caps.get("host_protocol_version") != 1:
163
+ raise ComputerUseError("Computer Use 组件版本不兼容,请更新组件。")
164
+ if self._caps.get("instance_id") != caps["instance_id"]:
165
+ self.connection_id = str(uuid4())
166
+ self._caps = caps
167
+
168
+ def capabilities(self, host_id: str | None) -> dict[str, Any]:
169
+ return dict(self._caps) if not self._closed and host_id == self.host_id else {}
170
+
171
+ def is_available(self, host_id: str | None, mode: str | None = None) -> bool:
172
+ caps = self.capabilities(host_id)
173
+ return bool(caps.get("gui_available") and (mode is None or mode in caps.get("supported_modes", [])))
174
+
175
+ async def execute(
176
+ self, host_id: str, *, session_id: str, agent_id: str | None,
177
+ action: dict[str, Any], mode: str | None = None, target_scope: str | None = None,
178
+ delivery_policy: str = "strict_background",
179
+ ) -> dict[str, Any]:
180
+ scope = target_scope or ("desktop" if mode == "foreground_desktop" else "app_window")
181
+ expected_mode = "foreground_desktop" if scope == "desktop" else "background_app"
182
+ if scope not in ("desktop", "app_window") or (mode is not None and mode != expected_mode):
183
+ raise ComputerUseError("Invalid Computer Use mode or target scope")
184
+ if delivery_policy not in ("strict_background", "allow_foreground"):
185
+ raise ComputerUseError("Invalid Computer Use delivery policy")
186
+ if scope == "desktop" and delivery_policy == "strict_background":
187
+ raise ComputerUseError("Desktop scope requires allow_foreground")
188
+ if not session_id or not self.is_available(host_id, expected_mode):
189
+ raise ComputerUseError("Computer Use is unavailable for this session or mode")
190
+ if action.get("action") not in ("observe", "list_windows", "list_displays", "wait") and self._caps.get("delivery_policy_version") != 1:
191
+ raise ComputerUseError("Computer Use host must be upgraded to enforce the delivery policy")
192
+ owner = {"host_id": self.host_id, "connection_id": self.connection_id,
193
+ "session_id": session_id, "agent_id": agent_id}
194
+ request = {"mode": expected_mode, "target_scope": scope,
195
+ "delivery_policy": delivery_policy, "action": action}
196
+ if self._caps.get("ax_protocol_version") == 1:
197
+ request["owner"] = owner
198
+ instance = self._caps["instance_id"]
199
+ result = await self.client.request("execute", {
200
+ "instanceId": instance, "lease": owner, "request": request,
201
+ }, instance_id=instance)
202
+ if not isinstance(result, dict):
203
+ raise ComputerUseError("Computer Use 执行器返回了无效结果")
204
+ # CLI has no monitor; monitor-only frames must not enter model/history.
205
+ result.pop("preview_screenshot", None)
206
+ result.pop("preview_screenshot_error", None)
207
+ return result
208
+
209
+ async def release(
210
+ self, host_id: str, *, session_id: str | None = None,
211
+ agent_id: str | None = None, all_agents: bool = False,
212
+ ) -> None:
213
+ if host_id != self.host_id or not self._caps:
214
+ return
215
+ await self.client.request("release", {
216
+ "hostId": self.host_id, "connectionId": self.connection_id,
217
+ "sessionId": session_id, "agentId": agent_id, "allAgents": all_agents,
218
+ }, instance_id=self._caps["instance_id"], timeout=5)
219
+
220
+ async def _keep_alive(self) -> None:
221
+ while True:
222
+ await asyncio.sleep(_HEARTBEAT_SECONDS)
223
+ try:
224
+ await self.client.request("heartbeat", {
225
+ "hostId": self.host_id, "connectionId": self.connection_id,
226
+ }, instance_id=self._caps.get("instance_id"), timeout=5)
227
+ except ComputerUseError:
228
+ self._caps["gui_available"] = False
229
+ try:
230
+ # Also recover a stopped executor and pick up permission changes.
231
+ # Only capabilities are refreshed; input is never retried.
232
+ await self.refresh()
233
+ except ComputerUseError:
234
+ self._caps["gui_available"] = False
235
+
236
+ async def close(self) -> None:
237
+ self._closed = True
238
+ if self._heartbeat is not None:
239
+ self._heartbeat.cancel()
240
+ with contextlib.suppress(asyncio.CancelledError):
241
+ await self._heartbeat
242
+ self._heartbeat = None
243
+ with contextlib.suppress(ComputerUseError):
244
+ await self.release(self.host_id, all_agents=True)
245
+ self._caps = {}
246
+
247
+
248
+ async def connect_session(session: Any) -> LocalComputerUse | None:
249
+ """Install the bundled component or reuse the shared one, without a launch flag."""
250
+ backend = LocalComputerUse()
251
+ if not backend.client.installed and backend.client.bundle is None:
252
+ return None
253
+ try:
254
+ await backend.connect()
255
+ except ComputerUseError as exc:
256
+ logger.warning("Computer Use unavailable: %s", exc)
257
+ await backend.close()
258
+ return None
259
+ session.computer_use_backend = backend
260
+ session.computer_use_host_id = backend.host_id
261
+ session.computer_use_enabled = True
262
+ return backend
263
+
264
+
265
+ def session_status(session: Any) -> str:
266
+ """Compact runtime state for /status; startup itself stays quiet."""
267
+ backend = getattr(session, "computer_use_backend", None)
268
+ host_id = getattr(session, "computer_use_host_id", None)
269
+ if backend is None:
270
+ client = NativeClient()
271
+ return "unavailable (run crabcode computer-use status)" if client.installed or client.bundle else "not installed"
272
+ if not getattr(session, "computer_use_enabled", False):
273
+ return "disabled"
274
+ caps = backend.capabilities(host_id)
275
+ mode = effective_computer_use_mode(session)
276
+ if not caps.get("gui_available"):
277
+ state = "unavailable"
278
+ elif not backend.is_available(host_id, mode):
279
+ state = f"unavailable in {mode}"
280
+ else:
281
+ state = "ready" if caps.get("input_available") else "limited"
282
+ details = " · ".join(
283
+ f"{label}={'yes' if caps.get(key) else 'no'}"
284
+ for key, label in (("capture_available", "capture"), ("input_available", "input"), ("ax_available", "AX"))
285
+ )
286
+ return f"{state} · {details}"