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,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())