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,66 @@
1
+ """TeamBridge — controlled cross-team communication.
2
+
3
+ Allows teams to send messages to each other through a bridge policy.
4
+ Policies: allow_all, allow_tagged, deny (default).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Awaitable, Callable
10
+
11
+ from crabcode_core.logging_utils import get_logger
12
+ from crabcode_core.team.manager import TeamManager
13
+ from crabcode_core.team.models import BridgePolicy, CrossTeamMessage
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class TeamBridge:
19
+ """Manages cross-team communication with policy-based access control.
20
+
21
+ Usage:
22
+ bridge = TeamBridge(team_manager)
23
+ bridge.register("team-a", "team-b", BridgePolicy.ALLOW_TAGGED)
24
+ await bridge.send("team-a", "agent-1", "team-b", "agent-2", "Hello!")
25
+ """
26
+
27
+ def __init__(self, team_manager: TeamManager) -> None:
28
+ self._manager = team_manager
29
+
30
+ def register(
31
+ self,
32
+ team_a: str,
33
+ team_b: str,
34
+ policy: BridgePolicy = BridgePolicy.ALLOW_TAGGED,
35
+ ) -> None:
36
+ """Register a bridge policy between two teams."""
37
+ self._manager.register_bridge(team_a, team_b, policy)
38
+
39
+ def get_policy(self, team_a: str, team_b: str) -> BridgePolicy:
40
+ """Get the bridge policy between two teams."""
41
+ return self._manager.get_bridge_policy(team_a, team_b)
42
+
43
+ async def send(
44
+ self,
45
+ from_team: str,
46
+ from_agent: str,
47
+ to_team: str,
48
+ to_agent: str,
49
+ text: str,
50
+ ) -> CrossTeamMessage | None:
51
+ """Send a cross-team message. Returns None if the bridge policy denies it."""
52
+ return await self._manager.send_cross_team(
53
+ from_team, from_agent, to_team, to_agent, text,
54
+ )
55
+
56
+ async def broadcast_to_team(
57
+ self,
58
+ from_team: str,
59
+ from_agent: str,
60
+ to_team: str,
61
+ text: str,
62
+ ) -> CrossTeamMessage | None:
63
+ """Broadcast a message to all members of another team."""
64
+ return await self._manager.send_cross_team(
65
+ from_team, from_agent, to_team, "", text,
66
+ )
@@ -0,0 +1,237 @@
1
+ """Inbox persistence — JSONL-based message storage for team inboxes.
2
+
3
+ Provides O(1) append writes and bulk read/mark_read operations.
4
+ This is the low-level storage layer; the TeamMessageBus uses it internally.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import tempfile
13
+ import threading
14
+ from contextlib import contextmanager
15
+ from pathlib import Path
16
+ from typing import Iterator
17
+
18
+ from crabcode_core.file_lock import file_lock
19
+ from crabcode_core.logging_utils import get_logger
20
+ from crabcode_core.path_validation import validate_path_component
21
+ from crabcode_core.team.models import TeamMessage
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ class InboxStorage:
26
+ """Manages JSONL inbox files for a team's agents.
27
+
28
+ Directory structure:
29
+ <root>/<team_name>/<agent_id>.jsonl
30
+
31
+ Each line in the JSONL file is a serialized TeamMessage.
32
+ Writes are O(1) (append). mark_read rewrites the full file,
33
+ but only fires once per prompt loop completion, not per message.
34
+ """
35
+
36
+ def __init__(self, root: Path) -> None:
37
+ self._root = root
38
+ self._locks: dict[tuple[str, str], threading.Lock] = {}
39
+
40
+ @staticmethod
41
+ def _validate_component(value: str, label: str) -> None:
42
+ validate_path_component(value, label)
43
+
44
+ def _lock_for(self, team_name: str, agent_id: str) -> threading.Lock:
45
+ return self._locks.setdefault((team_name, agent_id), threading.Lock())
46
+
47
+ @contextmanager
48
+ def _team_file_lock(self, team_name: str, *, shared: bool = False) -> Iterator[None]:
49
+ """Coordinate every operation for a team, including directory removal.
50
+
51
+ The lock lives beside the team directory rather than inside it. A
52
+ lock file inside the directory would be deleted by ``delete_team``;
53
+ a writer that resumed afterward could then create a new lock inode and
54
+ recreate the supposedly deleted inbox.
55
+ """
56
+ self._root.mkdir(parents=True, exist_ok=True)
57
+ lock_path = self._root / f".{team_name}.team.lock"
58
+ with file_lock(lock_path, exclusive=not shared):
59
+ yield
60
+
61
+ def inbox_path(self, team_name: str, agent_id: str) -> Path:
62
+ self._validate_component(team_name, "team name")
63
+ self._validate_component(agent_id, "agent id")
64
+ return self._root / team_name / f"{agent_id}.jsonl"
65
+
66
+ def write(self, team_name: str, agent_id: str, message: TeamMessage) -> None:
67
+ """Append a single message to an agent's inbox (O(1))."""
68
+ path = self.inbox_path(team_name, agent_id)
69
+ with self._lock_for(team_name, agent_id):
70
+ with self._team_file_lock(team_name):
71
+ path.parent.mkdir(parents=True, exist_ok=True)
72
+ line = message.model_dump_json() + "\n"
73
+ lock_path = path.with_name(f".{path.name}.lock")
74
+ with file_lock(lock_path):
75
+ with open(path, "a", encoding="utf-8") as f:
76
+ f.write(line)
77
+ f.flush()
78
+ os.fsync(f.fileno())
79
+
80
+ async def async_write(self, team_name: str, agent_id: str, message: TeamMessage) -> None:
81
+ """Async version of write."""
82
+ loop = asyncio.get_event_loop()
83
+ operation = loop.run_in_executor(None, self.write, team_name, agent_id, message)
84
+ try:
85
+ await asyncio.shield(operation)
86
+ except asyncio.CancelledError:
87
+ # A thread-pool write cannot be cancelled safely. Drain it before
88
+ # propagating cancellation so a caller cannot delete or replace the
89
+ # inbox while the append is still in flight.
90
+ await asyncio.shield(operation)
91
+ raise
92
+
93
+ def read_all(self, team_name: str, agent_id: str) -> list[TeamMessage]:
94
+ """Read all messages from an agent's inbox."""
95
+ path = self.inbox_path(team_name, agent_id)
96
+ with self._lock_for(team_name, agent_id):
97
+ with self._team_file_lock(team_name, shared=True):
98
+ try:
99
+ return self._read_all_locked(path)
100
+ except FileNotFoundError:
101
+ # Team cleanup may remove the file before a shared reader
102
+ # acquires the per-file lock.
103
+ return []
104
+
105
+ @staticmethod
106
+ def _read_all_locked(path: Path) -> list[TeamMessage]:
107
+ """Read a file while its caller holds the logical inbox lock."""
108
+ messages: list[TeamMessage] = []
109
+ lock_path = path.with_name(f".{path.name}.lock")
110
+ with file_lock(lock_path, exclusive=False):
111
+ with open(path, encoding="utf-8") as f:
112
+ for line in f:
113
+ line = line.strip()
114
+ if not line:
115
+ continue
116
+ try:
117
+ messages.append(TeamMessage.model_validate_json(line))
118
+ except Exception:
119
+ logger.debug("Skipping invalid inbox line", exc_info=True)
120
+ return messages
121
+
122
+ def mark_read(self, team_name: str, agent_id: str, message_ids: set[str] | None = None) -> int:
123
+ """Mark messages as read and rewrite the inbox file.
124
+
125
+ If message_ids is None, marks all as read.
126
+ Returns the count of messages marked read.
127
+ """
128
+ path = self.inbox_path(team_name, agent_id)
129
+ with self._lock_for(team_name, agent_id):
130
+ with self._team_file_lock(team_name):
131
+ try:
132
+ messages = self._read_all_locked(path)
133
+ except FileNotFoundError:
134
+ return 0
135
+ count = 0
136
+ for msg in messages:
137
+ if msg.read:
138
+ continue
139
+ if message_ids is not None and msg.id not in message_ids:
140
+ continue
141
+ msg.read = True
142
+ count += 1
143
+
144
+ if count > 0:
145
+ self._rewrite(path, messages)
146
+ return count
147
+
148
+ def delete_team(self, team_name: str) -> None:
149
+ """Delete all inbox files for a team."""
150
+ self._validate_component(team_name, "team name")
151
+ import shutil
152
+ with self._team_file_lock(team_name):
153
+ team_dir = self._root / team_name
154
+ if team_dir.exists():
155
+ shutil.rmtree(team_dir, ignore_errors=True)
156
+
157
+ def _rewrite(self, path: Path, messages: list[TeamMessage]) -> None:
158
+ """Rewrite the full inbox file (used by mark_read)."""
159
+ lines = [msg.model_dump_json() + "\n" for msg in messages]
160
+ path.parent.mkdir(parents=True, exist_ok=True)
161
+ lock_path = path.with_name(f".{path.name}.lock")
162
+ with file_lock(lock_path):
163
+ current = ""
164
+ if path.exists():
165
+ try:
166
+ current = path.read_text(encoding="utf-8")
167
+ except OSError:
168
+ current = ""
169
+ desired = "".join(lines)
170
+ merged = self._merge_jsonl(current, desired)
171
+ tmp_path: str | None = None
172
+ try:
173
+ with tempfile.NamedTemporaryFile(
174
+ mode="w",
175
+ encoding="utf-8",
176
+ dir=path.parent,
177
+ prefix=f".{path.name}.",
178
+ suffix=".tmp",
179
+ delete=False,
180
+ ) as f:
181
+ tmp_path = f.name
182
+ f.write(merged)
183
+ f.flush()
184
+ os.fsync(f.fileno())
185
+ os.replace(tmp_path, path)
186
+ tmp_path = None
187
+ finally:
188
+ if tmp_path is not None:
189
+ try:
190
+ os.unlink(tmp_path)
191
+ except OSError:
192
+ pass
193
+
194
+ @staticmethod
195
+ def _merge_jsonl(current: str, desired: str) -> str:
196
+ desired_by_id: dict[str, str] = {}
197
+ for raw in desired.splitlines():
198
+ if not raw.strip():
199
+ continue
200
+ try:
201
+ message_id = str(json.loads(raw).get("id", ""))
202
+ except Exception:
203
+ message_id = ""
204
+ if message_id:
205
+ desired_by_id[message_id] = raw
206
+
207
+ merged: list[str] = []
208
+ for raw in current.splitlines():
209
+ if not raw.strip():
210
+ continue
211
+ try:
212
+ message_id = str(json.loads(raw).get("id", ""))
213
+ except Exception:
214
+ message_id = ""
215
+ if message_id and message_id in desired_by_id:
216
+ merged.append(
217
+ InboxStorage._merge_json_line(
218
+ raw,
219
+ desired_by_id.pop(message_id),
220
+ )
221
+ )
222
+ else:
223
+ merged.append(raw)
224
+ merged.extend(desired_by_id.values())
225
+ return "".join(f"{line}\n" for line in merged)
226
+
227
+ @staticmethod
228
+ def _merge_json_line(current: str, desired: str) -> str:
229
+ """Merge a line without allowing a read acknowledgement to regress."""
230
+ try:
231
+ current_obj = json.loads(current)
232
+ desired_obj = json.loads(desired)
233
+ if current_obj.get("read"):
234
+ desired_obj["read"] = True
235
+ return json.dumps(desired_obj, ensure_ascii=False, separators=(",", ":"))
236
+ except Exception:
237
+ return desired