crabcode 0.1.0__py3-none-any.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 (64) hide show
  1. crabcode-0.1.0.dist-info/METADATA +23 -0
  2. crabcode-0.1.0.dist-info/RECORD +64 -0
  3. crabcode-0.1.0.dist-info/WHEEL +4 -0
  4. crabcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +3 -0
  6. crabcode_cli/app.py +142 -0
  7. crabcode_cli/banner.py +42 -0
  8. crabcode_cli/pipe.py +58 -0
  9. crabcode_cli/render/__init__.py +0 -0
  10. crabcode_cli/render/messages.py +19 -0
  11. crabcode_cli/repl.py +1263 -0
  12. crabcode_core/__init__.py +3 -0
  13. crabcode_core/api/__init__.py +6 -0
  14. crabcode_core/api/anthropic_adapter.py +250 -0
  15. crabcode_core/api/base.py +66 -0
  16. crabcode_core/api/codex_adapter.py +390 -0
  17. crabcode_core/api/openai_adapter.py +221 -0
  18. crabcode_core/api/registry.py +40 -0
  19. crabcode_core/compact/__init__.py +7 -0
  20. crabcode_core/compact/compact.py +150 -0
  21. crabcode_core/config/__init__.py +3 -0
  22. crabcode_core/config/claudemd.py +53 -0
  23. crabcode_core/config/manager.py +152 -0
  24. crabcode_core/events.py +392 -0
  25. crabcode_core/mcp/__init__.py +4 -0
  26. crabcode_core/mcp/client.py +163 -0
  27. crabcode_core/mcp/config.py +52 -0
  28. crabcode_core/permissions/__init__.py +3 -0
  29. crabcode_core/permissions/manager.py +106 -0
  30. crabcode_core/prompts/__init__.py +0 -0
  31. crabcode_core/prompts/context.py +209 -0
  32. crabcode_core/prompts/profile.py +66 -0
  33. crabcode_core/prompts/system.py +405 -0
  34. crabcode_core/prompts/templates.py +60 -0
  35. crabcode_core/query/__init__.py +0 -0
  36. crabcode_core/query/loop.py +442 -0
  37. crabcode_core/session/__init__.py +3 -0
  38. crabcode_core/session/storage.py +153 -0
  39. crabcode_core/skills/__init__.py +5 -0
  40. crabcode_core/skills/loader.py +147 -0
  41. crabcode_core/tools/__init__.py +29 -0
  42. crabcode_core/tools/agent.py +160 -0
  43. crabcode_core/tools/bash.py +127 -0
  44. crabcode_core/tools/diff_utils.py +120 -0
  45. crabcode_core/tools/file_edit.py +145 -0
  46. crabcode_core/tools/file_read.py +123 -0
  47. crabcode_core/tools/file_write.py +102 -0
  48. crabcode_core/tools/glob.py +109 -0
  49. crabcode_core/tools/grep.py +130 -0
  50. crabcode_core/tools/lint.py +368 -0
  51. crabcode_core/tools/memory.py +236 -0
  52. crabcode_core/tools/skill.py +112 -0
  53. crabcode_core/types/__init__.py +73 -0
  54. crabcode_core/types/config.py +103 -0
  55. crabcode_core/types/event.py +105 -0
  56. crabcode_core/types/message.py +177 -0
  57. crabcode_core/types/tool.py +138 -0
  58. crabcode_search/__init__.py +11 -0
  59. crabcode_search/background.py +371 -0
  60. crabcode_search/chunker.py +517 -0
  61. crabcode_search/embedder.py +447 -0
  62. crabcode_search/indexer.py +212 -0
  63. crabcode_search/store.py +222 -0
  64. crabcode_search/tool.py +232 -0
@@ -0,0 +1,3 @@
1
+ """CrabCode Core - AI coding assistant engine."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """API adapter layer - abstracts multiple LLM backends."""
2
+
3
+ from crabcode_core.api.base import APIAdapter, StreamChunk, ModelConfig
4
+ from crabcode_core.api.registry import create_adapter
5
+
6
+ __all__ = ["APIAdapter", "StreamChunk", "ModelConfig", "create_adapter"]
@@ -0,0 +1,250 @@
1
+ """Anthropic API adapter — primary backend, closest to original behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any, AsyncGenerator
8
+
9
+ import anthropic
10
+
11
+ from crabcode_core.api.base import APIAdapter, ModelConfig, StreamChunk
12
+ from crabcode_core.types.config import ApiConfig
13
+ from crabcode_core.types.message import (
14
+ ContentBlock,
15
+ Message,
16
+ MessageRole,
17
+ TextBlock,
18
+ ThinkingBlock,
19
+ ToolResultBlock,
20
+ ToolUseBlock,
21
+ )
22
+
23
+
24
+ def _messages_to_api(messages: list[Message]) -> list[dict[str, Any]]:
25
+ """Convert internal messages to Anthropic API format."""
26
+ result: list[dict[str, Any]] = []
27
+ for msg in messages:
28
+ if msg.role == MessageRole.SYSTEM:
29
+ continue
30
+
31
+ if isinstance(msg.content, str):
32
+ result.append({"role": msg.role.value, "content": msg.content})
33
+ continue
34
+
35
+ blocks: list[dict[str, Any]] = []
36
+ for block in msg.content:
37
+ if isinstance(block, TextBlock):
38
+ blocks.append({"type": "text", "text": block.text})
39
+ elif isinstance(block, ToolUseBlock):
40
+ blocks.append({
41
+ "type": "tool_use",
42
+ "id": block.id,
43
+ "name": block.name,
44
+ "input": block.input,
45
+ })
46
+ elif isinstance(block, ToolResultBlock):
47
+ blocks.append({
48
+ "type": "tool_result",
49
+ "tool_use_id": block.tool_use_id,
50
+ "content": block.content,
51
+ **({"is_error": True} if block.is_error else {}),
52
+ })
53
+ elif isinstance(block, ThinkingBlock):
54
+ blocks.append({
55
+ "type": "thinking",
56
+ "thinking": block.thinking,
57
+ })
58
+
59
+ if blocks:
60
+ result.append({"role": msg.role.value, "content": blocks})
61
+
62
+ return result
63
+
64
+
65
+ def _tools_to_api(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
66
+ """Convert tool schemas to Anthropic API format."""
67
+ result = []
68
+ for tool in tools:
69
+ api_tool: dict[str, Any] = {
70
+ "name": tool["name"],
71
+ "description": tool.get("description", ""),
72
+ "input_schema": tool.get("input_schema", {"type": "object", "properties": {}}),
73
+ }
74
+ result.append(api_tool)
75
+ return result
76
+
77
+
78
+ class AnthropicAdapter(APIAdapter):
79
+ """Adapter for Anthropic's Messages API (direct, first-party)."""
80
+
81
+ def __init__(self, config: ApiConfig):
82
+ self.config = config
83
+ api_key = None
84
+ if config.api_key_env:
85
+ api_key = os.environ.get(config.api_key_env)
86
+ if not api_key:
87
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
88
+
89
+ kwargs: dict[str, Any] = {}
90
+ if api_key:
91
+ kwargs["api_key"] = api_key
92
+ if config.base_url:
93
+ kwargs["base_url"] = config.base_url
94
+
95
+ self.client = anthropic.AsyncAnthropic(**kwargs)
96
+
97
+ async def stream_message(
98
+ self,
99
+ messages: list[Message],
100
+ system: list[str],
101
+ tools: list[dict[str, Any]],
102
+ config: ModelConfig,
103
+ ) -> AsyncGenerator[StreamChunk, None]:
104
+ model = config.model or self.config.model
105
+ if not model:
106
+ raise ValueError(
107
+ "No model configured. Set api.model in ~/.crabcode/settings.json or use the -m flag."
108
+ )
109
+
110
+ system_blocks = [{"type": "text", "text": s} for s in system if s]
111
+
112
+ params: dict[str, Any] = {
113
+ "model": model,
114
+ "max_tokens": config.max_tokens,
115
+ "system": system_blocks,
116
+ "messages": _messages_to_api(messages)
117
+ }
118
+
119
+ api_tools = _tools_to_api(tools)
120
+ if api_tools:
121
+ params["tools"] = api_tools
122
+
123
+ if config.thinking_enabled:
124
+ params["thinking"] = {
125
+ "type": "enabled",
126
+ "budget_tokens": config.thinking_budget,
127
+ }
128
+
129
+ if config.temperature is not None:
130
+ params["temperature"] = config.temperature
131
+
132
+ current_tool_id = ""
133
+ current_tool_name = ""
134
+ tool_input_buffer = ""
135
+
136
+ async with self.client.messages.stream(**params) as stream:
137
+ async for event in stream:
138
+ event_type = getattr(event, "type", "")
139
+
140
+ if event_type == "content_block_start":
141
+ block = event.content_block
142
+ if hasattr(block, "type"):
143
+ if block.type == "tool_use":
144
+ current_tool_id = block.id
145
+ current_tool_name = block.name
146
+ tool_input_buffer = ""
147
+ yield StreamChunk(
148
+ type="tool_use_start",
149
+ tool_use_id=block.id,
150
+ tool_name=block.name,
151
+ )
152
+ elif block.type == "thinking":
153
+ pass
154
+ elif block.type == "text":
155
+ pass
156
+
157
+ elif event_type == "content_block_delta":
158
+ delta = event.delta
159
+ if hasattr(delta, "type"):
160
+ if delta.type == "text_delta":
161
+ yield StreamChunk(type="text", text=delta.text)
162
+ elif delta.type == "thinking_delta":
163
+ yield StreamChunk(type="thinking", text=delta.thinking)
164
+ elif delta.type == "input_json_delta":
165
+ tool_input_buffer += delta.partial_json
166
+ yield StreamChunk(
167
+ type="tool_use_delta",
168
+ tool_use_id=current_tool_id,
169
+ tool_input_json=delta.partial_json,
170
+ )
171
+
172
+ elif event_type == "content_block_stop":
173
+ if current_tool_id:
174
+ yield StreamChunk(
175
+ type="tool_use_end",
176
+ tool_use_id=current_tool_id,
177
+ tool_name=current_tool_name,
178
+ tool_input_json=tool_input_buffer,
179
+ )
180
+ current_tool_id = ""
181
+ current_tool_name = ""
182
+ tool_input_buffer = ""
183
+
184
+ elif event_type == "message_delta":
185
+ usage = {}
186
+ if hasattr(event, "usage") and event.usage:
187
+ usage = {
188
+ "output_tokens": getattr(event.usage, "output_tokens", 0),
189
+ }
190
+ stop_reason = getattr(event.delta, "stop_reason", "") or ""
191
+ yield StreamChunk(
192
+ type="message_delta",
193
+ stop_reason=stop_reason,
194
+ usage=usage,
195
+ )
196
+
197
+ elif event_type == "message_start":
198
+ usage = {}
199
+ if hasattr(event.message, "usage") and event.message.usage:
200
+ u = event.message.usage
201
+ usage = {
202
+ "input_tokens": getattr(u, "input_tokens", 0),
203
+ "output_tokens": getattr(u, "output_tokens", 0),
204
+ }
205
+ yield StreamChunk(type="message_start", usage=usage)
206
+
207
+ elif event_type == "message_stop":
208
+ yield StreamChunk(type="message_stop")
209
+
210
+ async def count_tokens(
211
+ self,
212
+ messages: list[Message],
213
+ system: list[str],
214
+ ) -> int:
215
+ try:
216
+ result = await self.client.messages.count_tokens(
217
+ model=self.config.model or "",
218
+ messages=_messages_to_api(messages),
219
+ system=[{"type": "text", "text": s} for s in system if s],
220
+ )
221
+ return result.input_tokens
222
+ except Exception:
223
+ total = sum(len(s) for s in system)
224
+ for msg in messages:
225
+ if isinstance(msg.content, str):
226
+ total += len(msg.content)
227
+ else:
228
+ for block in msg.content:
229
+ if isinstance(block, TextBlock):
230
+ total += len(block.text)
231
+ return total // 4
232
+
233
+
234
+ class BedrockAdapter(AnthropicAdapter):
235
+ """Adapter for Anthropic via AWS Bedrock."""
236
+
237
+ def __init__(self, config: ApiConfig):
238
+ self.config = config
239
+ self.client = anthropic.AsyncAnthropicBedrock()
240
+
241
+
242
+ class VertexAdapter(AnthropicAdapter):
243
+ """Adapter for Anthropic via Google Vertex AI."""
244
+
245
+ def __init__(self, config: ApiConfig):
246
+ self.config = config
247
+ self.client = anthropic.AsyncAnthropicVertex(
248
+ region=os.environ.get("CLOUD_ML_REGION", "us-east5"),
249
+ project_id=os.environ.get("ANTHROPIC_VERTEX_PROJECT_ID"),
250
+ )
@@ -0,0 +1,66 @@
1
+ """Abstract base for all API adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, AsyncGenerator
8
+
9
+ from crabcode_core.types.message import ContentBlock, Message
10
+
11
+
12
+ @dataclass
13
+ class ModelConfig:
14
+ """Configuration for a single API call."""
15
+ model: str = "claude-sonnet-4-20250514"
16
+ max_tokens: int = 16384
17
+ thinking_enabled: bool = True
18
+ thinking_budget: int = 10000
19
+ temperature: float | None = None
20
+ stop_sequences: list[str] | None = None
21
+ timeout: int = 300 # seconds
22
+
23
+
24
+ @dataclass
25
+ class StreamChunk:
26
+ """A single chunk from the streaming API response.
27
+
28
+ Each chunk can carry text, tool use data, thinking, or metadata.
29
+ """
30
+ type: str # "text" | "tool_use_start" | "tool_use_delta" | "tool_use_end" | "thinking" | "message_start" | "message_delta" | "message_stop" | "error"
31
+ text: str = ""
32
+ tool_use_id: str = ""
33
+ tool_name: str = ""
34
+ tool_input_json: str = ""
35
+ usage: dict[str, Any] = field(default_factory=dict)
36
+ stop_reason: str = ""
37
+ error: str = ""
38
+
39
+
40
+ class APIAdapter(ABC):
41
+ """Abstract interface for LLM API backends.
42
+
43
+ All adapters translate between CrabCode's internal Anthropic-style
44
+ message format and the provider's native format.
45
+ """
46
+
47
+ @abstractmethod
48
+ async def stream_message(
49
+ self,
50
+ messages: list[Message],
51
+ system: list[str],
52
+ tools: list[dict[str, Any]],
53
+ config: ModelConfig,
54
+ ) -> AsyncGenerator[StreamChunk, None]:
55
+ """Send messages and stream back response chunks."""
56
+ ...
57
+ yield # pragma: no cover
58
+
59
+ @abstractmethod
60
+ async def count_tokens(
61
+ self,
62
+ messages: list[Message],
63
+ system: list[str],
64
+ ) -> int:
65
+ """Estimate token count for a message list."""
66
+ ...