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