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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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())
|