sari 0.0.1__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.
- app/__init__.py +1 -0
- app/config.py +240 -0
- app/db.py +932 -0
- app/dedup_queue.py +77 -0
- app/engine_registry.py +56 -0
- app/engine_runtime.py +472 -0
- app/http_server.py +204 -0
- app/indexer.py +1532 -0
- app/main.py +147 -0
- app/models.py +39 -0
- app/queue_pipeline.py +65 -0
- app/ranking.py +144 -0
- app/registry.py +172 -0
- app/search_engine.py +572 -0
- app/watcher.py +124 -0
- app/workspace.py +286 -0
- deckard/__init__.py +3 -0
- deckard/__main__.py +4 -0
- deckard/main.py +345 -0
- deckard/version.py +1 -0
- mcp/__init__.py +1 -0
- mcp/__main__.py +19 -0
- mcp/cli.py +485 -0
- mcp/daemon.py +149 -0
- mcp/proxy.py +304 -0
- mcp/registry.py +218 -0
- mcp/server.py +519 -0
- mcp/session.py +234 -0
- mcp/telemetry.py +112 -0
- mcp/test_cli.py +89 -0
- mcp/test_daemon.py +124 -0
- mcp/test_server.py +197 -0
- mcp/tools/__init__.py +14 -0
- mcp/tools/_util.py +244 -0
- mcp/tools/deckard_guide.py +32 -0
- mcp/tools/doctor.py +208 -0
- mcp/tools/get_callers.py +60 -0
- mcp/tools/get_implementations.py +60 -0
- mcp/tools/index_file.py +75 -0
- mcp/tools/list_files.py +138 -0
- mcp/tools/read_file.py +48 -0
- mcp/tools/read_symbol.py +99 -0
- mcp/tools/registry.py +212 -0
- mcp/tools/repo_candidates.py +89 -0
- mcp/tools/rescan.py +46 -0
- mcp/tools/scan_once.py +54 -0
- mcp/tools/search.py +208 -0
- mcp/tools/search_api_endpoints.py +72 -0
- mcp/tools/search_symbols.py +63 -0
- mcp/tools/status.py +135 -0
- sari/__init__.py +1 -0
- sari/__main__.py +4 -0
- sari-0.0.1.dist-info/METADATA +521 -0
- sari-0.0.1.dist-info/RECORD +58 -0
- sari-0.0.1.dist-info/WHEEL +5 -0
- sari-0.0.1.dist-info/entry_points.txt +2 -0
- sari-0.0.1.dist-info/licenses/LICENSE +21 -0
- sari-0.0.1.dist-info/top_level.txt +4 -0
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Local Search App
|
app/config.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from .workspace import WorkspaceManager # type: ignore
|
|
11
|
+
except ImportError:
|
|
12
|
+
from workspace import WorkspaceManager # type: ignore
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _expanduser(p: str) -> str:
|
|
18
|
+
return os.path.expanduser(p)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Config:
|
|
23
|
+
workspace_root: str # Primary root
|
|
24
|
+
server_host: str
|
|
25
|
+
server_port: int
|
|
26
|
+
scan_interval_seconds: int
|
|
27
|
+
snippet_max_lines: int
|
|
28
|
+
max_file_bytes: int
|
|
29
|
+
db_path: str
|
|
30
|
+
include_ext: list[str]
|
|
31
|
+
include_files: list[str]
|
|
32
|
+
exclude_dirs: list[str]
|
|
33
|
+
exclude_globs: list[str]
|
|
34
|
+
redact_enabled: bool
|
|
35
|
+
commit_batch_size: int
|
|
36
|
+
http_api_host: str = "127.0.0.1"
|
|
37
|
+
http_api_port: int = 7331
|
|
38
|
+
exclude_content_bytes: int = 104857600
|
|
39
|
+
workspace_roots: list[str] = field(default_factory=list) # Optional for compatibility
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
# Ensure workspace_roots is always a list containing at least workspace_root
|
|
43
|
+
if not self.workspace_roots:
|
|
44
|
+
object.__setattr__(self, "workspace_roots", [self.workspace_root])
|
|
45
|
+
elif self.workspace_root not in self.workspace_roots:
|
|
46
|
+
# Sync workspace_root to be the first of roots
|
|
47
|
+
object.__setattr__(self, "workspace_root", self.workspace_roots[0])
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_defaults(workspace_root: str) -> dict:
|
|
51
|
+
"""Central source for default configuration values (v2.7.0)."""
|
|
52
|
+
return {
|
|
53
|
+
"workspace_roots": [workspace_root],
|
|
54
|
+
"workspace_root": workspace_root,
|
|
55
|
+
"server_host": "127.0.0.1",
|
|
56
|
+
"server_port": 47777,
|
|
57
|
+
"http_api_host": "127.0.0.1",
|
|
58
|
+
"http_api_port": 7331,
|
|
59
|
+
"scan_interval_seconds": 180,
|
|
60
|
+
"snippet_max_lines": 5,
|
|
61
|
+
"max_file_bytes": 1000000, # Increased to 1MB
|
|
62
|
+
"db_path": str(WorkspaceManager.get_local_db_path(workspace_root)),
|
|
63
|
+
"include_ext": [".py", ".js", ".ts", ".java", ".kt", ".go", ".rs", ".md", ".json", ".yaml", ".yml", ".sh"],
|
|
64
|
+
"include_files": ["pom.xml", "package.json", "Dockerfile", "Makefile", "build.gradle", "settings.gradle"],
|
|
65
|
+
"exclude_dirs": [".git", "node_modules", "__pycache__", ".venv", "venv", "target", "build", "dist", "coverage", "vendor"],
|
|
66
|
+
"exclude_globs": [
|
|
67
|
+
"*.min.js",
|
|
68
|
+
"*.min.css",
|
|
69
|
+
"*.map",
|
|
70
|
+
"*.lock",
|
|
71
|
+
"package-lock.json",
|
|
72
|
+
"yarn.lock",
|
|
73
|
+
"pnpm-lock.yaml",
|
|
74
|
+
"*.class",
|
|
75
|
+
"*.pyc",
|
|
76
|
+
"*.pyo",
|
|
77
|
+
"__pycache__/*",
|
|
78
|
+
],
|
|
79
|
+
"redact_enabled": True,
|
|
80
|
+
"commit_batch_size": 500,
|
|
81
|
+
"exclude_content_bytes": 104857600, # 100MB default for full content storage
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def save_paths_only(self, path: str, extra_paths: dict = None) -> None:
|
|
85
|
+
"""Persist resolved path-related configuration to disk (Write-back)."""
|
|
86
|
+
extra_paths = extra_paths or {}
|
|
87
|
+
data = {}
|
|
88
|
+
# Load existing config to preserve non-path settings
|
|
89
|
+
if path and os.path.exists(path):
|
|
90
|
+
try:
|
|
91
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
92
|
+
data = json.load(f) or {}
|
|
93
|
+
except Exception:
|
|
94
|
+
data = {}
|
|
95
|
+
if not isinstance(data, dict):
|
|
96
|
+
data = {}
|
|
97
|
+
|
|
98
|
+
data["roots"] = self.workspace_roots
|
|
99
|
+
data["db_path"] = self.db_path
|
|
100
|
+
# Optional path keys (if provided)
|
|
101
|
+
for k, v in extra_paths.items():
|
|
102
|
+
if v:
|
|
103
|
+
data[k] = v
|
|
104
|
+
try:
|
|
105
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
107
|
+
json.dump(data, f, indent=2)
|
|
108
|
+
logger.info(f"Configuration saved to {path}")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to save configuration to {path}: {e}")
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def load(path: str, workspace_root_override: str = None, root_uri: str = None) -> "Config":
|
|
114
|
+
raw = {}
|
|
115
|
+
if path and os.path.exists(path):
|
|
116
|
+
try:
|
|
117
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
118
|
+
raw = json.load(f)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to load config from {path}: {e}")
|
|
121
|
+
|
|
122
|
+
# Backward compatibility: legacy "indexing" schema
|
|
123
|
+
legacy_indexing = raw.get("indexing", {}) if isinstance(raw, dict) else {}
|
|
124
|
+
if "include_ext" not in raw and "include_extensions" in legacy_indexing:
|
|
125
|
+
raw = dict(raw)
|
|
126
|
+
raw["include_ext"] = legacy_indexing.get("include_extensions", [])
|
|
127
|
+
if "exclude_dirs" not in raw and "exclude_patterns" in legacy_indexing:
|
|
128
|
+
raw = dict(raw)
|
|
129
|
+
legacy_excludes = list(legacy_indexing.get("exclude_patterns", []))
|
|
130
|
+
raw["exclude_dirs"] = legacy_excludes
|
|
131
|
+
if "exclude_globs" not in raw:
|
|
132
|
+
raw["exclude_globs"] = [p for p in legacy_excludes if any(c in p for c in ["*", "?"])]
|
|
133
|
+
|
|
134
|
+
# --- Multi-root Resolution ---
|
|
135
|
+
# Sources for roots:
|
|
136
|
+
# 1. workspace_root_override (argument) -> treated as a root
|
|
137
|
+
# 2. Config file 'roots' or 'workspace_roots' (list)
|
|
138
|
+
# 3. Config file 'workspace_root' (str) -> legacy
|
|
139
|
+
config_roots = raw.get("roots") or raw.get("workspace_roots") or []
|
|
140
|
+
if not config_roots and raw.get("workspace_root"):
|
|
141
|
+
config_roots = [raw.get("workspace_root")]
|
|
142
|
+
|
|
143
|
+
# Resolve base roots (env + config + legacy + optional root_uri).
|
|
144
|
+
final_roots = WorkspaceManager.resolve_workspace_roots(
|
|
145
|
+
root_uri=root_uri,
|
|
146
|
+
config_roots=config_roots
|
|
147
|
+
)
|
|
148
|
+
if workspace_root_override:
|
|
149
|
+
follow_symlinks = (os.environ.get("DECKARD_FOLLOW_SYMLINKS", "0").strip().lower() in ("1", "true", "yes", "on"))
|
|
150
|
+
try:
|
|
151
|
+
override_norm = WorkspaceManager._normalize_path(workspace_root_override, follow_symlinks=follow_symlinks) # type: ignore
|
|
152
|
+
except Exception:
|
|
153
|
+
override_norm = workspace_root_override
|
|
154
|
+
# Put override first, keep others after
|
|
155
|
+
final_roots = [override_norm] + [r for r in final_roots if r != override_norm]
|
|
156
|
+
|
|
157
|
+
if not final_roots:
|
|
158
|
+
final_roots = [os.getcwd()]
|
|
159
|
+
|
|
160
|
+
primary_root = final_roots[0]
|
|
161
|
+
|
|
162
|
+
defaults = Config.get_defaults(primary_root)
|
|
163
|
+
|
|
164
|
+
# Support port override (legacy)
|
|
165
|
+
port_override = os.environ.get("DECKARD_PORT") or os.environ.get("LOCAL_SEARCH_PORT_OVERRIDE")
|
|
166
|
+
server_port = int(port_override) if port_override else int(raw.get("server_port", defaults["server_port"]))
|
|
167
|
+
|
|
168
|
+
# HTTP API port override (SSOT)
|
|
169
|
+
http_port_override = (
|
|
170
|
+
os.environ.get("DECKARD_HTTP_API_PORT")
|
|
171
|
+
or os.environ.get("DECKARD_HTTP_PORT")
|
|
172
|
+
or os.environ.get("LOCAL_SEARCH_HTTP_PORT")
|
|
173
|
+
)
|
|
174
|
+
http_api_port = int(http_port_override) if http_port_override else int(raw.get("http_api_port", defaults["http_api_port"]))
|
|
175
|
+
|
|
176
|
+
# Unified DB path resolution
|
|
177
|
+
# Priority: Env > Config File > Default(primary_root)
|
|
178
|
+
env_db_path = (os.environ.get("DECKARD_DB_PATH") or os.environ.get("LOCAL_SEARCH_DB_PATH") or "").strip()
|
|
179
|
+
|
|
180
|
+
db_path = ""
|
|
181
|
+
if env_db_path:
|
|
182
|
+
expanded = _expanduser(env_db_path)
|
|
183
|
+
if os.path.isabs(expanded):
|
|
184
|
+
db_path = expanded
|
|
185
|
+
else:
|
|
186
|
+
logger.warning(f"Ignoring relative DB_PATH '{env_db_path}'. Absolute path required.")
|
|
187
|
+
|
|
188
|
+
if not db_path:
|
|
189
|
+
raw_db_path = raw.get("db_path", "")
|
|
190
|
+
# Check if packaged config logic applies (skip relative path check for packaged default?)
|
|
191
|
+
# Simplified: just expand and use if absolute
|
|
192
|
+
if raw_db_path:
|
|
193
|
+
expanded = _expanduser(raw_db_path)
|
|
194
|
+
if os.path.isabs(expanded):
|
|
195
|
+
db_path = expanded
|
|
196
|
+
|
|
197
|
+
if not db_path:
|
|
198
|
+
db_path = str(WorkspaceManager.get_local_db_path(primary_root))
|
|
199
|
+
|
|
200
|
+
# --- Construct Config Object ---
|
|
201
|
+
cfg = Config(
|
|
202
|
+
workspace_roots=final_roots,
|
|
203
|
+
workspace_root=primary_root,
|
|
204
|
+
server_host=raw.get("server_host", defaults["server_host"]),
|
|
205
|
+
server_port=server_port,
|
|
206
|
+
http_api_host=raw.get("http_api_host", defaults["http_api_host"]),
|
|
207
|
+
http_api_port=http_api_port,
|
|
208
|
+
scan_interval_seconds=int(raw.get("scan_interval_seconds", defaults["scan_interval_seconds"])),
|
|
209
|
+
snippet_max_lines=int(raw.get("snippet_max_lines", defaults["snippet_max_lines"])),
|
|
210
|
+
max_file_bytes=int(raw.get("max_file_bytes", defaults["max_file_bytes"])),
|
|
211
|
+
db_path=_expanduser(db_path),
|
|
212
|
+
include_ext=list(raw.get("include_ext", defaults["include_ext"])),
|
|
213
|
+
include_files=list(raw.get("include_files", defaults["include_files"])),
|
|
214
|
+
exclude_dirs=list(raw.get("exclude_dirs", defaults["exclude_dirs"])),
|
|
215
|
+
exclude_globs=list(raw.get("exclude_globs", defaults["exclude_globs"])),
|
|
216
|
+
redact_enabled=bool(raw.get("redact_enabled", defaults["redact_enabled"])),
|
|
217
|
+
commit_batch_size=int(raw.get("commit_batch_size", defaults["commit_batch_size"])),
|
|
218
|
+
exclude_content_bytes=int(raw.get("exclude_content_bytes", defaults["exclude_content_bytes"])),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# --- Write-back (Persist) Logic ---
|
|
222
|
+
persist_flag = os.environ.get("DECKARD_PERSIST_ROOTS", os.environ.get("DECKARD_PERSIST_PATHS", "0")).strip().lower()
|
|
223
|
+
should_persist = persist_flag in ("1", "true", "yes", "on")
|
|
224
|
+
|
|
225
|
+
if should_persist and path:
|
|
226
|
+
extra = {
|
|
227
|
+
"install_dir": (os.environ.get("DECKARD_INSTALL_DIR") or "").strip(),
|
|
228
|
+
"data_dir": (os.environ.get("DECKARD_DATA_DIR") or "").strip(),
|
|
229
|
+
"db_path": (os.environ.get("DECKARD_DB_PATH") or "").strip(),
|
|
230
|
+
"config_path": (os.environ.get("DECKARD_CONFIG") or "").strip(),
|
|
231
|
+
}
|
|
232
|
+
cfg.save_paths_only(path, extra_paths=extra)
|
|
233
|
+
|
|
234
|
+
return cfg
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def resolve_config_path(repo_root: str) -> str:
|
|
239
|
+
"""Resolve config path using unified WorkspaceManager logic."""
|
|
240
|
+
return WorkspaceManager.resolve_config_path(repo_root)
|