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.
Files changed (58) hide show
  1. app/__init__.py +1 -0
  2. app/config.py +240 -0
  3. app/db.py +932 -0
  4. app/dedup_queue.py +77 -0
  5. app/engine_registry.py +56 -0
  6. app/engine_runtime.py +472 -0
  7. app/http_server.py +204 -0
  8. app/indexer.py +1532 -0
  9. app/main.py +147 -0
  10. app/models.py +39 -0
  11. app/queue_pipeline.py +65 -0
  12. app/ranking.py +144 -0
  13. app/registry.py +172 -0
  14. app/search_engine.py +572 -0
  15. app/watcher.py +124 -0
  16. app/workspace.py +286 -0
  17. deckard/__init__.py +3 -0
  18. deckard/__main__.py +4 -0
  19. deckard/main.py +345 -0
  20. deckard/version.py +1 -0
  21. mcp/__init__.py +1 -0
  22. mcp/__main__.py +19 -0
  23. mcp/cli.py +485 -0
  24. mcp/daemon.py +149 -0
  25. mcp/proxy.py +304 -0
  26. mcp/registry.py +218 -0
  27. mcp/server.py +519 -0
  28. mcp/session.py +234 -0
  29. mcp/telemetry.py +112 -0
  30. mcp/test_cli.py +89 -0
  31. mcp/test_daemon.py +124 -0
  32. mcp/test_server.py +197 -0
  33. mcp/tools/__init__.py +14 -0
  34. mcp/tools/_util.py +244 -0
  35. mcp/tools/deckard_guide.py +32 -0
  36. mcp/tools/doctor.py +208 -0
  37. mcp/tools/get_callers.py +60 -0
  38. mcp/tools/get_implementations.py +60 -0
  39. mcp/tools/index_file.py +75 -0
  40. mcp/tools/list_files.py +138 -0
  41. mcp/tools/read_file.py +48 -0
  42. mcp/tools/read_symbol.py +99 -0
  43. mcp/tools/registry.py +212 -0
  44. mcp/tools/repo_candidates.py +89 -0
  45. mcp/tools/rescan.py +46 -0
  46. mcp/tools/scan_once.py +54 -0
  47. mcp/tools/search.py +208 -0
  48. mcp/tools/search_api_endpoints.py +72 -0
  49. mcp/tools/search_symbols.py +63 -0
  50. mcp/tools/status.py +135 -0
  51. sari/__init__.py +1 -0
  52. sari/__main__.py +4 -0
  53. sari-0.0.1.dist-info/METADATA +521 -0
  54. sari-0.0.1.dist-info/RECORD +58 -0
  55. sari-0.0.1.dist-info/WHEEL +5 -0
  56. sari-0.0.1.dist-info/entry_points.txt +2 -0
  57. sari-0.0.1.dist-info/licenses/LICENSE +21 -0
  58. 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)