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,11 @@
|
|
|
1
|
+
"""Semantic codebase search for CrabCode."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["CodebaseSearchTool"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name: str):
|
|
7
|
+
if name == "CodebaseSearchTool":
|
|
8
|
+
from crabcode_search.tool import CodebaseSearchTool
|
|
9
|
+
|
|
10
|
+
return CodebaseSearchTool
|
|
11
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Background prewarm/index process for CodebaseSearch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
# Must be set before ANY native library (FAISS / PyTorch) loads libomp.
|
|
16
|
+
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
|
|
17
|
+
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
|
18
|
+
os.environ.setdefault("MKL_NUM_THREADS", "1")
|
|
19
|
+
|
|
20
|
+
STATUS_FILE_NAME = "background-status.json"
|
|
21
|
+
PID_FILE_NAME = "background.pid"
|
|
22
|
+
LOG_FILE_NAME = "background.log"
|
|
23
|
+
CODEBASE_SEARCH_TOOL = "crabcode_search.CodebaseSearchTool"
|
|
24
|
+
BACKGROUND_LOGS_DIR = ".crabcode/logs"
|
|
25
|
+
BACKGROUND_LOG_INDEX = "index.json"
|
|
26
|
+
CODEBASE_SEARCH_LOG_KEY = "search"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_codebase_search_enabled(extra_tools: list[str]) -> bool:
|
|
30
|
+
"""Return True if CodebaseSearch is configured as an extra tool."""
|
|
31
|
+
return CODEBASE_SEARCH_TOOL in extra_tools
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
STALE_TIMEOUT_SECONDS = 300
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def maybe_spawn_background_indexer(
|
|
38
|
+
cwd: str,
|
|
39
|
+
tool_config: dict[str, Any] | None = None,
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Start a detached background indexer if one is not already running."""
|
|
42
|
+
index_dir = Path(cwd).resolve() / ".crabcode" / "search"
|
|
43
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
pid_file = index_dir / PID_FILE_NAME
|
|
46
|
+
status_file = index_dir / STATUS_FILE_NAME
|
|
47
|
+
log_file = get_background_log_path(cwd, CODEBASE_SEARCH_LOG_KEY)
|
|
48
|
+
|
|
49
|
+
existing_pid = _read_pid(pid_file)
|
|
50
|
+
if existing_pid and _pid_is_running(existing_pid):
|
|
51
|
+
if _is_stale(status_file):
|
|
52
|
+
try:
|
|
53
|
+
import signal
|
|
54
|
+
os.kill(existing_pid, signal.SIGTERM)
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
pid_file.unlink(missing_ok=True)
|
|
58
|
+
else:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
if pid_file.exists():
|
|
62
|
+
pid_file.unlink(missing_ok=True)
|
|
63
|
+
|
|
64
|
+
_write_status(
|
|
65
|
+
status_file,
|
|
66
|
+
{
|
|
67
|
+
"state": "starting",
|
|
68
|
+
"cwd": str(Path(cwd).resolve()),
|
|
69
|
+
"pid": None,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Avoid `python -m crabcode_search.background` — the __main__ / package
|
|
74
|
+
# double-init in `-m` mode breaks PyTorch MPS on macOS, causing
|
|
75
|
+
# model.encode() to hang indefinitely.
|
|
76
|
+
cmd = [
|
|
77
|
+
sys.executable,
|
|
78
|
+
"-c",
|
|
79
|
+
"from crabcode_search.background import main; raise SystemExit(main())",
|
|
80
|
+
"--cwd",
|
|
81
|
+
str(Path(cwd).resolve()),
|
|
82
|
+
"--tool-config-json",
|
|
83
|
+
json.dumps(tool_config or {}, ensure_ascii=True),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
env = os.environ.copy()
|
|
87
|
+
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
88
|
+
env.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
89
|
+
|
|
90
|
+
with open(log_file, "ab") as log:
|
|
91
|
+
proc = subprocess.Popen(
|
|
92
|
+
cmd,
|
|
93
|
+
stdin=subprocess.DEVNULL,
|
|
94
|
+
stdout=log,
|
|
95
|
+
stderr=log,
|
|
96
|
+
close_fds=True,
|
|
97
|
+
env=env,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
_write_status(
|
|
101
|
+
status_file,
|
|
102
|
+
{
|
|
103
|
+
"state": "starting",
|
|
104
|
+
"cwd": str(Path(cwd).resolve()),
|
|
105
|
+
"pid": proc.pid,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
pid_file.write_text(str(proc.pid), encoding="utf-8")
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_background_status(cwd: str) -> dict[str, Any] | None:
|
|
113
|
+
"""Read the current background indexer status from disk."""
|
|
114
|
+
status_file = Path(cwd).resolve() / ".crabcode" / "search" / STATUS_FILE_NAME
|
|
115
|
+
if not status_file.exists():
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(status_file.read_text(encoding="utf-8"))
|
|
119
|
+
except (OSError, json.JSONDecodeError):
|
|
120
|
+
return None
|
|
121
|
+
return data if isinstance(data, dict) else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_background_logs_dir(cwd: str) -> Path:
|
|
125
|
+
"""Return the directory that stores background tool logs."""
|
|
126
|
+
logs_dir = Path(cwd).resolve() / BACKGROUND_LOGS_DIR
|
|
127
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
return logs_dir
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_background_log_path(cwd: str, key: str) -> Path:
|
|
132
|
+
"""Return the log path for a background tool key and register it."""
|
|
133
|
+
logs_dir = get_background_logs_dir(cwd)
|
|
134
|
+
log_path = logs_dir / f"{key}.log"
|
|
135
|
+
_register_background_log(cwd, key, log_path)
|
|
136
|
+
return log_path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_background_logs(cwd: str) -> dict[str, str]:
|
|
140
|
+
"""Return registered background logs as {key: absolute_path}."""
|
|
141
|
+
index_path = get_background_logs_dir(cwd) / BACKGROUND_LOG_INDEX
|
|
142
|
+
|
|
143
|
+
legacy = Path(cwd).resolve() / ".crabcode" / "search" / LOG_FILE_NAME
|
|
144
|
+
if not index_path.exists() and legacy.exists():
|
|
145
|
+
_register_background_log(cwd, CODEBASE_SEARCH_LOG_KEY, legacy)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
data = json.loads(index_path.read_text(encoding="utf-8"))
|
|
149
|
+
except (OSError, json.JSONDecodeError):
|
|
150
|
+
return {}
|
|
151
|
+
if not isinstance(data, dict):
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
result: dict[str, str] = {}
|
|
155
|
+
for key, value in data.items():
|
|
156
|
+
if not (isinstance(key, str) and isinstance(value, str)):
|
|
157
|
+
continue
|
|
158
|
+
path = Path(value)
|
|
159
|
+
if not path.exists() and key == CODEBASE_SEARCH_LOG_KEY and legacy.exists():
|
|
160
|
+
result[key] = str(legacy)
|
|
161
|
+
_register_background_log(cwd, key, legacy)
|
|
162
|
+
else:
|
|
163
|
+
result[key] = value
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _run_background_indexer(cwd: str, tool_config: dict[str, Any]) -> int:
|
|
168
|
+
from crabcode_search.embedder import create_embedder
|
|
169
|
+
from crabcode_search.indexer import CodebaseIndexer
|
|
170
|
+
|
|
171
|
+
threads = tool_config.get("threads")
|
|
172
|
+
try:
|
|
173
|
+
import torch
|
|
174
|
+
|
|
175
|
+
n = int(threads) if threads else max(os.cpu_count() or 4, 4)
|
|
176
|
+
torch.set_num_threads(n)
|
|
177
|
+
except ImportError:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
index_dir = Path(cwd).resolve() / ".crabcode" / "search"
|
|
181
|
+
status_file = index_dir / STATUS_FILE_NAME
|
|
182
|
+
pid_file = index_dir / PID_FILE_NAME
|
|
183
|
+
|
|
184
|
+
pid_file.write_text(str(os.getpid()), encoding="utf-8")
|
|
185
|
+
|
|
186
|
+
_write_status(
|
|
187
|
+
status_file,
|
|
188
|
+
{
|
|
189
|
+
"state": "preloading",
|
|
190
|
+
"cwd": str(Path(cwd).resolve()),
|
|
191
|
+
"pid": os.getpid(),
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Force CPU in the background subprocess: MPS hangs when
|
|
197
|
+
# stdout/stderr are redirected to files (no TTY / Metal context).
|
|
198
|
+
device = tool_config.get("device") or "cpu"
|
|
199
|
+
|
|
200
|
+
embedder = create_embedder(
|
|
201
|
+
backend=tool_config.get("embedder", "ollama"),
|
|
202
|
+
model=tool_config.get("model"),
|
|
203
|
+
dimension=tool_config.get("dimension"),
|
|
204
|
+
batch_size=tool_config.get("batch_size"),
|
|
205
|
+
device=device,
|
|
206
|
+
)
|
|
207
|
+
await embedder.preload()
|
|
208
|
+
|
|
209
|
+
_write_status(
|
|
210
|
+
status_file,
|
|
211
|
+
{
|
|
212
|
+
"state": "scanning",
|
|
213
|
+
"cwd": str(Path(cwd).resolve()),
|
|
214
|
+
"pid": os.getpid(),
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
indexer = CodebaseIndexer(cwd, embedder=embedder)
|
|
219
|
+
all_files = indexer._scan_files()
|
|
220
|
+
indexer.total_files = len(all_files)
|
|
221
|
+
|
|
222
|
+
changed = indexer._detect_changes(all_files)
|
|
223
|
+
|
|
224
|
+
if indexer.store.count > 0 and not changed:
|
|
225
|
+
_write_status(
|
|
226
|
+
status_file,
|
|
227
|
+
{
|
|
228
|
+
"state": "ready",
|
|
229
|
+
"cwd": str(Path(cwd).resolve()),
|
|
230
|
+
"pid": os.getpid(),
|
|
231
|
+
"chunks": indexer.store.count,
|
|
232
|
+
"files": indexer.total_files,
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
_write_status(
|
|
238
|
+
status_file,
|
|
239
|
+
{
|
|
240
|
+
"state": "indexing",
|
|
241
|
+
"cwd": str(Path(cwd).resolve()),
|
|
242
|
+
"pid": os.getpid(),
|
|
243
|
+
"done": 0,
|
|
244
|
+
"total": len(changed),
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async for progress in indexer.build_or_update():
|
|
249
|
+
_write_status(
|
|
250
|
+
status_file,
|
|
251
|
+
{
|
|
252
|
+
"state": "indexing",
|
|
253
|
+
"cwd": str(Path(cwd).resolve()),
|
|
254
|
+
"pid": os.getpid(),
|
|
255
|
+
"done": progress.done,
|
|
256
|
+
"total": progress.total,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
_write_status(
|
|
261
|
+
status_file,
|
|
262
|
+
{
|
|
263
|
+
"state": "ready",
|
|
264
|
+
"cwd": str(Path(cwd).resolve()),
|
|
265
|
+
"pid": os.getpid(),
|
|
266
|
+
"chunks": indexer.store.count,
|
|
267
|
+
"files": indexer.total_files,
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
return 0
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
_write_status(
|
|
273
|
+
status_file,
|
|
274
|
+
{
|
|
275
|
+
"state": "error",
|
|
276
|
+
"cwd": str(Path(cwd).resolve()),
|
|
277
|
+
"pid": os.getpid(),
|
|
278
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
279
|
+
"traceback": traceback.format_exc(),
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
return 1
|
|
283
|
+
finally:
|
|
284
|
+
current_pid = _read_pid(pid_file)
|
|
285
|
+
if current_pid == os.getpid():
|
|
286
|
+
pid_file.unlink(missing_ok=True)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _read_pid(pid_file: Path) -> int | None:
|
|
290
|
+
try:
|
|
291
|
+
raw = pid_file.read_text(encoding="utf-8").strip()
|
|
292
|
+
except OSError:
|
|
293
|
+
return None
|
|
294
|
+
if not raw:
|
|
295
|
+
return None
|
|
296
|
+
try:
|
|
297
|
+
return int(raw)
|
|
298
|
+
except ValueError:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _pid_is_running(pid: int) -> bool:
|
|
303
|
+
try:
|
|
304
|
+
os.kill(pid, 0)
|
|
305
|
+
except ProcessLookupError:
|
|
306
|
+
return False
|
|
307
|
+
except PermissionError:
|
|
308
|
+
return True
|
|
309
|
+
except OSError:
|
|
310
|
+
return False
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _is_stale(status_file: Path) -> bool:
|
|
315
|
+
"""Return True if the status file hasn't been updated for too long."""
|
|
316
|
+
try:
|
|
317
|
+
mtime = status_file.stat().st_mtime
|
|
318
|
+
except OSError:
|
|
319
|
+
return True
|
|
320
|
+
import time
|
|
321
|
+
return (time.time() - mtime) > STALE_TIMEOUT_SECONDS
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _write_status(status_file: Path, payload: dict[str, Any]) -> None:
|
|
325
|
+
status_file.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
status_file.write_text(
|
|
327
|
+
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
328
|
+
encoding="utf-8",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _register_background_log(cwd: str, key: str, log_path: Path) -> None:
|
|
333
|
+
index_path = get_background_logs_dir(cwd) / BACKGROUND_LOG_INDEX
|
|
334
|
+
try:
|
|
335
|
+
data = json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else {}
|
|
336
|
+
except (OSError, json.JSONDecodeError):
|
|
337
|
+
data = {}
|
|
338
|
+
if not isinstance(data, dict):
|
|
339
|
+
data = {}
|
|
340
|
+
data[key] = str(log_path.resolve())
|
|
341
|
+
index_path.write_text(
|
|
342
|
+
json.dumps(data, ensure_ascii=False, indent=2),
|
|
343
|
+
encoding="utf-8",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def main(argv: list[str] | None = None) -> int:
|
|
348
|
+
parser = argparse.ArgumentParser(
|
|
349
|
+
description="Background prewarm/index process for CodebaseSearch.",
|
|
350
|
+
)
|
|
351
|
+
parser.add_argument("--cwd", required=True, help="Repository root to index")
|
|
352
|
+
parser.add_argument(
|
|
353
|
+
"--tool-config-json",
|
|
354
|
+
default="{}",
|
|
355
|
+
help="JSON object for tool_settings.CodebaseSearch",
|
|
356
|
+
)
|
|
357
|
+
args = parser.parse_args(argv)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
tool_config = json.loads(args.tool_config_json)
|
|
361
|
+
if not isinstance(tool_config, dict):
|
|
362
|
+
raise ValueError("tool config must be a JSON object")
|
|
363
|
+
except Exception as exc:
|
|
364
|
+
sys.stderr.write(f"Invalid --tool-config-json: {exc}\n")
|
|
365
|
+
return 2
|
|
366
|
+
|
|
367
|
+
return asyncio.run(_run_background_indexer(args.cwd, tool_config))
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
raise SystemExit(main())
|