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.
- crabcode-0.1.0.dist-info/METADATA +23 -0
- crabcode-0.1.0.dist-info/RECORD +64 -0
- crabcode-0.1.0.dist-info/WHEEL +4 -0
- crabcode-0.1.0.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +3 -0
- crabcode_cli/app.py +142 -0
- crabcode_cli/banner.py +42 -0
- crabcode_cli/pipe.py +58 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +1263 -0
- crabcode_core/__init__.py +3 -0
- crabcode_core/api/__init__.py +6 -0
- crabcode_core/api/anthropic_adapter.py +250 -0
- crabcode_core/api/base.py +66 -0
- crabcode_core/api/codex_adapter.py +390 -0
- crabcode_core/api/openai_adapter.py +221 -0
- crabcode_core/api/registry.py +40 -0
- crabcode_core/compact/__init__.py +7 -0
- crabcode_core/compact/compact.py +150 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +53 -0
- crabcode_core/config/manager.py +152 -0
- crabcode_core/events.py +392 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +163 -0
- crabcode_core/mcp/config.py +52 -0
- crabcode_core/permissions/__init__.py +3 -0
- crabcode_core/permissions/manager.py +106 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/context.py +209 -0
- crabcode_core/prompts/profile.py +66 -0
- crabcode_core/prompts/system.py +405 -0
- crabcode_core/prompts/templates.py +60 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/loop.py +442 -0
- crabcode_core/session/__init__.py +3 -0
- crabcode_core/session/storage.py +153 -0
- crabcode_core/skills/__init__.py +5 -0
- crabcode_core/skills/loader.py +147 -0
- crabcode_core/tools/__init__.py +29 -0
- crabcode_core/tools/agent.py +160 -0
- crabcode_core/tools/bash.py +127 -0
- crabcode_core/tools/diff_utils.py +120 -0
- crabcode_core/tools/file_edit.py +145 -0
- crabcode_core/tools/file_read.py +123 -0
- crabcode_core/tools/file_write.py +102 -0
- crabcode_core/tools/glob.py +109 -0
- crabcode_core/tools/grep.py +130 -0
- crabcode_core/tools/lint.py +368 -0
- crabcode_core/tools/memory.py +236 -0
- crabcode_core/tools/skill.py +112 -0
- crabcode_core/types/__init__.py +73 -0
- crabcode_core/types/config.py +103 -0
- crabcode_core/types/event.py +105 -0
- crabcode_core/types/message.py +177 -0
- crabcode_core/types/tool.py +138 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +371 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +447 -0
- crabcode_search/indexer.py +212 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +232 -0
|
@@ -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
|
+
...
|