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,434 @@
1
+ """Configuration types for CrabCode settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, Field, field_validator, model_validator
9
+
10
+
11
+ DEFAULT_FILESYSTEM_TIMEOUT = 3600.0
12
+ DEFAULT_COMPACT_BUFFER_TOKENS = 20_000
13
+
14
+
15
+ ReasoningEffort = Literal[
16
+ "none", "minimal", "low", "medium", "high", "xhigh", "max"
17
+ ]
18
+ REASONING_EFFORT_LEVELS: tuple[ReasoningEffort, ...] = (
19
+ "none",
20
+ "minimal",
21
+ "low",
22
+ "medium",
23
+ "high",
24
+ "xhigh",
25
+ "max",
26
+ )
27
+
28
+
29
+ class PermissionRule(BaseModel):
30
+ """A single permission rule for tool access control."""
31
+ tool: str
32
+ path: str | None = None
33
+ command: str | None = None
34
+
35
+
36
+ class AiReviewSettings(BaseModel):
37
+ """Settings for AI-assisted tool permission review."""
38
+
39
+ model: str | None = None
40
+ decisions: list[Literal["allow", "ask", "deny"]] = Field(
41
+ default_factory=lambda: ["allow", "ask"]
42
+ )
43
+ fallback: Literal["allow", "ask", "deny"] = "ask"
44
+ timeout: int = 30
45
+
46
+
47
+ class PermissionsSettings(BaseModel):
48
+ allow: list[PermissionRule] = Field(default_factory=list)
49
+ deny: list[PermissionRule] = Field(default_factory=list)
50
+ ask: list[PermissionRule] = Field(default_factory=list)
51
+ # None means "not explicitly configured". PermissionManager still falls
52
+ # back to ask mode, while config layering can distinguish an omitted value
53
+ # from an explicit override.
54
+ default_mode: str | None = None
55
+ additional_directories: list[str] = Field(default_factory=list)
56
+ # Deprecated compatibility alias for default_mode="run_everything".
57
+ run_everything: bool = False
58
+ ai_review: AiReviewSettings = Field(default_factory=AiReviewSettings)
59
+
60
+
61
+ class McpServerConfig(BaseModel):
62
+ command: list[str] | None = None
63
+ url: str | None = None
64
+ env: dict[str, str] = Field(default_factory=dict)
65
+ disabled: bool = False
66
+
67
+
68
+ class LspServerConfig(BaseModel):
69
+ """Configuration for a single LSP server.
70
+
71
+ Users can customize built-in LSP servers or add new ones in settings.json,
72
+ following the same pattern as OpenCode's opencode.json LSP configuration.
73
+ """
74
+
75
+ command: list[str] = Field(default_factory=list)
76
+ extensions: list[str] = Field(default_factory=list)
77
+ env: dict[str, str] = Field(default_factory=dict)
78
+ initialization: dict[str, Any] = Field(default_factory=dict)
79
+ disabled: bool = False
80
+
81
+
82
+ class ApiConfig(BaseModel):
83
+ """API backend configuration."""
84
+ # Optional shared configuration entry. This is metadata used while
85
+ # loading ``CrabCodeSettings`` and is retained on the resolved config so
86
+ # callers can still identify its source group.
87
+ group: str | None = None
88
+ provider: str | None = None # anthropic | openai | codex | ollama | gemini | azure | bedrock | vertex | router
89
+ model: str | None = None
90
+ base_url: str | None = None
91
+ network_mode: Literal["inherit", "direct", "proxy"] = "inherit"
92
+ proxy_url: str | None = None
93
+
94
+ @field_validator("proxy_url")
95
+ @classmethod
96
+ def validate_proxy_url(cls, value: str | None) -> str | None:
97
+ if not value:
98
+ return None
99
+ from urllib.parse import urlsplit
100
+ try:
101
+ parsed = urlsplit(value)
102
+ valid = parsed.scheme in {"http", "https"} and bool(parsed.hostname) and parsed.port != 0
103
+ valid = valid and not parsed.username and not parsed.password
104
+ except ValueError:
105
+ valid = False
106
+ if not valid:
107
+ raise ValueError("Use an HTTP(S) proxy URL without embedded credentials")
108
+ return value
109
+
110
+ api_key_env: str | None = None
111
+ codex_auth_path: str | None = None
112
+ # Opt in to native Responses image generation on API-key/custom Codex
113
+ # endpoints. Official Codex OAuth exposes it automatically.
114
+ image_generation_enabled: bool = False
115
+ http_headers: dict[str, str] = Field(default_factory=dict)
116
+ format: str | None = None # anthropic | openai | codex | ollama | gemini | azure (for routers)
117
+ max_tokens: int = 16384
118
+ thinking_enabled: bool = True
119
+ thinking_budget: int = 10000
120
+ pass_reasoning_content: bool = False
121
+ anthropic_stream_transport: Literal["auto", "sdk", "httpx"] = "auto"
122
+ reasoning_effort: ReasoningEffort | None = None
123
+ timeout: int = 300 # seconds, for API calls
124
+ # HTTP request-establishment retries for transport failures and 5xx,
125
+ # excluding 429. This budget is separate from stream reconnects.
126
+ request_max_retries: int = Field(default=4, ge=0, le=100)
127
+ # Stream reconnect budget (default 3, hard cap 100).
128
+ max_retries: int = Field(default=3, ge=0, le=100)
129
+ # Explicit opt-in for a separate unbounded 5s..60s connection retry path.
130
+ unbounded_connection_retries: bool = False
131
+ context_window: int | None = None # override auto-detected context window
132
+ prompt_cache_key: str | None = None # OpenAI Responses API prompt cache routing key
133
+ prompt_caching: Literal["auto", "enabled", "disabled"] = "auto"
134
+ prompt_cache_retention: Literal["in_memory", "24h"] | None = None
135
+ extra_body: dict[str, Any] = Field(default_factory=dict)
136
+ azure_endpoint: str | None = None # Azure OpenAI endpoint URL
137
+ azure_api_version: str | None = None # Azure API version
138
+ azure_deployment: str | None = None # Azure deployment name (if model field is not used for this)
139
+
140
+
141
+ class AgentSettings(BaseModel):
142
+ """Settings for the built-in Agent (sub-agent) tool."""
143
+ max_turns: int = 10
144
+ timeout: int = 300
145
+ max_output_chars: int = 12000
146
+ stream_send_input_output: bool = False
147
+ max_concurrency: int = 4
148
+ max_depth: int = 2
149
+ max_active_agents_per_run: int = 16
150
+ types: dict[str, "AgentTypeConfig"] = Field(default_factory=dict)
151
+
152
+
153
+ class AgentTypeConfig(BaseModel):
154
+ """Settings for a specific sub-agent type."""
155
+ model_profile: str | None = None
156
+ allowed_tools: list[str] = Field(default_factory=list)
157
+ prompt: str | None = None
158
+ enable_lsp: bool = True
159
+
160
+
161
+ class DisplaySettings(BaseModel):
162
+ """Settings for tool result display in the terminal."""
163
+ default_max_lines: int = 50
164
+ tool_max_lines: dict[str, int] = {}
165
+ max_chars: int = 50_000
166
+
167
+ # Built-in defaults merged under tool_max_lines overrides
168
+ _TOOL_DEFAULTS: dict[str, int] = {
169
+ "Agent": 120,
170
+ "Bash": 60,
171
+ "apply_patch": 120,
172
+ "Grep": 50,
173
+ "Glob": 30,
174
+ "Read": 80,
175
+ "Lint": 60,
176
+ "WebSearch": 50,
177
+ "Browser": 60,
178
+ "CodebaseSearch": 50,
179
+ }
180
+
181
+ def get_max_lines(self, tool_name: str) -> int:
182
+ """Return max display lines for a tool, considering overrides."""
183
+ if tool_name in self.tool_max_lines:
184
+ return self.tool_max_lines[tool_name]
185
+ return self._TOOL_DEFAULTS.get(tool_name, self.default_max_lines)
186
+
187
+
188
+ class LoggingSettings(BaseModel):
189
+ """Settings for runtime logging."""
190
+
191
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING"
192
+ file: str | None = None
193
+
194
+
195
+ class TeamSettings(BaseModel):
196
+ """Settings for Agent Teams feature."""
197
+
198
+ max_teammates: int = Field(default=8, gt=0)
199
+ backpressure_queue_size: int = Field(default=100, gt=0)
200
+ max_message_size_bytes: int = Field(default=10_000, gt=0)
201
+ inbox_dir: str | None = None
202
+ bridge_policy: str = "deny" # deny | allow_tagged | allow_all
203
+
204
+
205
+ class CrossSessionSettings(BaseModel):
206
+ """Settings for messaging between independent local sessions."""
207
+
208
+ enabled: bool = True
209
+ name: str | None = None
210
+ inbound: Literal["auto", "accept", "hold", "refuse"] = "auto"
211
+ registry_dir: str | None = None
212
+ queue_size: int = Field(default=50, gt=0)
213
+ max_message_size_bytes: int = Field(default=10_000, gt=0)
214
+ connect_timeout_seconds: float = Field(default=3.0, gt=0)
215
+
216
+
217
+ class ScheduleSettings(BaseModel):
218
+ """Settings for the Schedule (cron/interval/once) subsystem."""
219
+
220
+ enabled: bool = True
221
+ max_concurrent_jobs: int = 4
222
+ default_timeout: int = 600 # seconds
223
+ max_runs_per_job: int | None = None # None = unlimited
224
+ persist: bool = True # persist jobs across restarts
225
+ log_retention_days: int = 30
226
+
227
+
228
+ class SnapshotSettings(BaseModel):
229
+ """Controls optional file-system snapshots used by Bash and Checkpoint."""
230
+
231
+ enabled: bool = True
232
+ # The scanner stops as soon as this cumulative size is exceeded. This is
233
+ # deliberately a safety ceiling rather than an exact disk-usage report.
234
+ max_size_mb: int = Field(default=1024, ge=1, le=1_048_576)
235
+
236
+
237
+ class ComputerUseSettings(BaseModel):
238
+ """Controls how a connected Computer Use host operates the GUI."""
239
+
240
+ target_scope: Literal["app_window", "desktop"] = "app_window"
241
+ delivery_policy: Literal["strict_background", "allow_foreground"] = "allow_foreground"
242
+
243
+ @model_validator(mode="before")
244
+ @classmethod
245
+ def migrate_mode(cls, value: Any) -> Any:
246
+ if isinstance(value, dict) and "mode" in value:
247
+ value = dict(value)
248
+ mode = value.pop("mode")
249
+ if mode not in ("background_app", "foreground_desktop"):
250
+ raise ValueError("Unknown Computer Use mode")
251
+ value.setdefault("target_scope", "desktop" if mode == "foreground_desktop" else "app_window")
252
+ return value
253
+
254
+ @property
255
+ def mode(self) -> str:
256
+ """Legacy routing alias. It never overrides foreground permission."""
257
+ return "foreground_desktop" if self.target_scope == "desktop" else "background_app"
258
+
259
+
260
+ class GatewaySecuritySettings(BaseModel):
261
+ """Authentication settings for the HTTP/WebSocket/gRPC gateway."""
262
+
263
+ mode: Literal["none", "password", "publickey", "mixed"] = "none"
264
+ # A plain password is accepted for local development; deployments should
265
+ # prefer password_hash or the CLI/environment instead.
266
+ password: str | None = None
267
+ password_hash: str | None = None
268
+ jwt_secret: str | None = None
269
+ authorized_keys: str = "~/.ssh/authorized_keys"
270
+ token_ttl_seconds: int = Field(default=900, gt=0, le=86_400)
271
+
272
+
273
+ class GatewayWorkspaceSettings(BaseModel):
274
+ """Directories the Gateway may expose through workspace discovery APIs."""
275
+
276
+ browse_roots: list[str] = Field(default_factory=list)
277
+
278
+
279
+ class GatewaySettings(BaseModel):
280
+ security: GatewaySecuritySettings = Field(default_factory=GatewaySecuritySettings)
281
+ workspace: GatewayWorkspaceSettings = Field(default_factory=GatewayWorkspaceSettings)
282
+
283
+
284
+ class ToolLoadingSettings(BaseModel):
285
+ mode: Literal["eager", "discovery"] = "discovery"
286
+ pinned_tools: list[str] = Field(default_factory=list)
287
+
288
+
289
+ class PromptTemplateConfig(BaseModel):
290
+ """A named system-prompt template. Blank sections mean the built-in default."""
291
+
292
+ id: str
293
+ name: str
294
+ sections: dict[str, str] = Field(default_factory=dict)
295
+
296
+ @field_validator("id", "name")
297
+ @classmethod
298
+ def _required_name(cls, value: str) -> str:
299
+ text = value.strip()
300
+ if not text:
301
+ raise ValueError("must not be blank")
302
+ if len(text) > 80:
303
+ raise ValueError("is too long")
304
+ return text
305
+
306
+ @field_validator("sections", mode="before")
307
+ @classmethod
308
+ def _section_text(cls, value: Any) -> dict[str, str]:
309
+ if not isinstance(value, dict):
310
+ return {}
311
+ cleaned: dict[str, str] = {}
312
+ for key, item in value.items():
313
+ if isinstance(key, str) and isinstance(item, str):
314
+ cleaned[key] = item
315
+ return cleaned
316
+
317
+
318
+ class UserAppendPromptConfig(BaseModel):
319
+ """A saved prompt that can be appended to the user message sent to the model."""
320
+
321
+ id: str
322
+ text: str
323
+ enabled: bool = False
324
+
325
+ @field_validator("id", "text")
326
+ @classmethod
327
+ def _required_text(cls, value: str) -> str:
328
+ text = value.strip()
329
+ if not text:
330
+ raise ValueError("must not be blank")
331
+ if len(text) > 8000:
332
+ raise ValueError("is too long")
333
+ return text
334
+
335
+
336
+ class CrabCodeSettings(BaseModel):
337
+ """Full settings.json schema."""
338
+ follow_up_mode: Literal["queue", "steer"] = "queue"
339
+ permissions: PermissionsSettings = Field(default_factory=PermissionsSettings)
340
+ env: dict[str, str] = Field(default_factory=dict)
341
+ mcp_servers: dict[str, McpServerConfig] = Field(default_factory=dict)
342
+ api: ApiConfig = Field(default_factory=ApiConfig)
343
+ # Shared API settings inherited by named models. A model's explicitly
344
+ # configured fields take precedence over the referenced group.
345
+ groups: dict[str, ApiConfig] = Field(default_factory=dict)
346
+ models: dict[str, ApiConfig] = Field(default_factory=dict)
347
+ default_model: str | None = None
348
+ hooks: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
349
+ auto_compact_enabled: bool = True
350
+ compact_buffer_tokens: int = Field(default=DEFAULT_COMPACT_BUFFER_TOKENS, ge=0, strict=True)
351
+ max_context_length: int | None = None
352
+ language: str | None = None
353
+ output_style: str | None = None
354
+ prompt_profile: dict[str, Any] | None = None
355
+ prompt_templates: list[PromptTemplateConfig] = Field(default_factory=list)
356
+ active_prompt_template: str | None = None
357
+ user_append_prompts: list[UserAppendPromptConfig] = Field(default_factory=list)
358
+ extra_tools: list[str] = Field(default_factory=list)
359
+ tool_loading: ToolLoadingSettings = Field(default_factory=ToolLoadingSettings)
360
+ ultra_mode: bool = False
361
+ tool_call_timeout: float | None = None
362
+ filesystem_timeout: float | None = Field(
363
+ default=DEFAULT_FILESYSTEM_TIMEOUT,
364
+ gt=0,
365
+ allow_inf_nan=False,
366
+ description="Default timeout in seconds for Read, apply_patch, Write, Edit, Glob, Grep, and Bash; null disables it.",
367
+ )
368
+ tool_settings: dict[str, dict[str, Any]] = Field(default_factory=dict)
369
+ agent: AgentSettings = Field(default_factory=AgentSettings)
370
+ team: TeamSettings = Field(default_factory=TeamSettings)
371
+ cross_session: CrossSessionSettings = Field(default_factory=CrossSessionSettings)
372
+ schedule: ScheduleSettings = Field(default_factory=ScheduleSettings)
373
+ snapshot: SnapshotSettings = Field(default_factory=SnapshotSettings)
374
+ computer_use: ComputerUseSettings = Field(default_factory=ComputerUseSettings)
375
+ gateway: GatewaySettings = Field(default_factory=GatewaySettings)
376
+ display: DisplaySettings = Field(default_factory=DisplaySettings)
377
+ logging: LoggingSettings = Field(default_factory=LoggingSettings)
378
+ lsp: dict[str, LspServerConfig] | bool = Field(default_factory=dict)
379
+
380
+ model_config = {"extra": "allow"}
381
+
382
+ @field_validator("active_prompt_template", mode="before")
383
+ @classmethod
384
+ def _blank_active_prompt_template(cls, value: Any) -> str | None:
385
+ if value is None:
386
+ return None
387
+ if isinstance(value, str):
388
+ text = value.strip()
389
+ return text or None
390
+ return value
391
+
392
+ @model_validator(mode="after")
393
+ def _resolve_model_groups(self) -> "CrabCodeSettings":
394
+ """Expand group settings into named models after validation.
395
+
396
+ Pydantic populates omitted fields with defaults, so using a normal
397
+ dictionary merge would incorrectly override group values (for example
398
+ an omitted ``thinking_enabled`` would look like ``True``). The
399
+ per-model ``model_fields_set`` records which fields the user actually
400
+ supplied and lets us inherit only the omitted fields.
401
+ """
402
+ if not self.groups or not self.models:
403
+ return self
404
+
405
+ resolved: dict[str, ApiConfig] = {}
406
+ for name, model_config in self.models.items():
407
+ group_name = model_config.group
408
+ group_config = self.groups.get(group_name) if group_name else None
409
+ if group_config is None:
410
+ # Keep unknown groups non-fatal so a typo does not discard all
411
+ # otherwise valid settings; the model remains usable on its
412
+ # own and callers can report the missing group if desired.
413
+ resolved[name] = model_config
414
+ continue
415
+
416
+ inherited = group_config.model_copy(deep=True)
417
+ for field_name in ApiConfig.model_fields:
418
+ if field_name in model_config.model_fields_set:
419
+ setattr(
420
+ inherited,
421
+ field_name,
422
+ deepcopy(getattr(model_config, field_name)),
423
+ )
424
+ resolved[name] = inherited
425
+
426
+ self.models = resolved
427
+ return self
428
+
429
+ def get_api_config(self, model_name: str | None = None) -> ApiConfig:
430
+ """Return the ApiConfig for a named model, falling back to the default api config."""
431
+ name = model_name or self.default_model
432
+ if name and name in self.models:
433
+ return self.models[name]
434
+ return self.api