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
@@ -0,0 +1,72 @@
1
+ import json
2
+ import sqlite3
3
+ from typing import Any, Dict, List
4
+ try:
5
+ from ._util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
6
+ except ImportError:
7
+ from _util import mcp_response, pack_header, pack_line, pack_encode_id, pack_encode_text, resolve_root_ids, pack_error, ErrorCode
8
+
9
+ def execute_search_api_endpoints(args: Dict[str, Any], db: Any, roots: List[str]) -> Dict[str, Any]:
10
+ """Search for API endpoints by URL path pattern."""
11
+ path_query = args.get("path", "").strip()
12
+ if not path_query:
13
+ return mcp_response(
14
+ "search_api_endpoints",
15
+ lambda: pack_error("search_api_endpoints", ErrorCode.INVALID_ARGS, "Path query is required"),
16
+ lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": "Path query is required"}, "isError": True},
17
+ )
18
+
19
+ # Search in symbols where metadata contains the path
20
+ # SQLite JSON support is limited in older versions, so we use LIKE on metadata TEXT
21
+ sql = """
22
+ SELECT path, name, kind, line, metadata, content
23
+ FROM symbols
24
+ WHERE metadata LIKE ? AND (kind = 'method' OR kind = 'function' OR kind = 'class')
25
+ """
26
+ # Look for partial matches in metadata (looser LIKE, filter in Python)
27
+ params = [f'%{path_query}%']
28
+ root_ids = resolve_root_ids(roots)
29
+ if root_ids:
30
+ root_clause = " OR ".join(["path LIKE ?"] * len(root_ids))
31
+ sql += f" AND ({root_clause})"
32
+ params.extend([f"{rid}/%" for rid in root_ids])
33
+
34
+ with db._read_lock:
35
+ rows = db._read.execute(sql, params).fetchall()
36
+
37
+ results = []
38
+ for r in rows:
39
+ try:
40
+ meta = json.loads(r["metadata"])
41
+ http_path = meta.get("http_path", "")
42
+ if path_query in http_path or path_query == http_path:
43
+ results.append({
44
+ "path": r["path"],
45
+ "name": r["name"],
46
+ "kind": r["kind"],
47
+ "line": r["line"],
48
+ "http_path": http_path,
49
+ "annotations": meta.get("annotations", []),
50
+ "snippet": r["content"]
51
+ })
52
+ except:
53
+ continue
54
+
55
+ def build_pack() -> str:
56
+ lines = [pack_header("search_api_endpoints", {"q": pack_encode_text(path_query)}, returned=len(results))]
57
+ for r in results:
58
+ kv = {
59
+ "path": pack_encode_id(r["path"]),
60
+ "name": pack_encode_id(r["name"]),
61
+ "kind": pack_encode_id(r["kind"]),
62
+ "line": str(r["line"]),
63
+ "http_path": pack_encode_text(r["http_path"]),
64
+ }
65
+ lines.append(pack_line("r", kv))
66
+ return "\n".join(lines)
67
+
68
+ return mcp_response(
69
+ "search_api_endpoints",
70
+ build_pack,
71
+ lambda: {"query": path_query, "results": results, "count": len(results)},
72
+ )
@@ -0,0 +1,63 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from app.db import LocalSearchDB
3
+ from mcp.tools._util import mcp_response, pack_header, pack_line, pack_truncated, pack_encode_id, pack_encode_text, resolve_root_ids
4
+
5
+ def execute_search_symbols(args: Dict[str, Any], db: LocalSearchDB, roots: Optional[List[str]] = None) -> Dict[str, Any]:
6
+ """
7
+ Execute search_symbols tool.
8
+
9
+ Args:
10
+ args: {"query": str, "limit": int}
11
+ db: LocalSearchDB instance
12
+ """
13
+ query = args.get("query", "")
14
+ limit_arg = int(args.get("limit", 20))
15
+ root_ids = resolve_root_ids(list(roots or []))
16
+
17
+ # --- JSON Builder (Legacy/Debug) ---
18
+ def build_json() -> Dict[str, Any]:
19
+ results = db.search_symbols(query, limit=limit_arg, root_ids=root_ids)
20
+ return {
21
+ "query": query,
22
+ "count": len(results),
23
+ "symbols": results
24
+ }
25
+
26
+ # --- PACK1 Builder ---
27
+ def build_pack() -> str:
28
+ # Hard limit for PACK1: 50
29
+ pack_limit = min(limit_arg, 50)
30
+
31
+ results = db.search_symbols(query, limit=pack_limit, root_ids=root_ids)
32
+ returned = len(results)
33
+
34
+ # Header
35
+ # Note: search_symbols DB query typically doesn't return total count currently
36
+ kv = {"q": pack_encode_text(query), "limit": pack_limit}
37
+ lines = [
38
+ pack_header("search_symbols", kv, returned=returned, total_mode="none")
39
+ ]
40
+
41
+ # Records
42
+ for r in results:
43
+ # h:repo=<repo> path=<path> line=<line> kind=<kind> name=<name>
44
+ # repo, path, name, kind => ENC_ID (identifiers)
45
+ kv_line = {
46
+ "repo": pack_encode_id(r["repo"]),
47
+ "path": pack_encode_id(r["path"]),
48
+ "line": str(r["line"]),
49
+ "kind": pack_encode_id(r["kind"]),
50
+ "name": pack_encode_id(r["name"])
51
+ }
52
+ lines.append(pack_line("h", kv_line))
53
+
54
+ # Truncation
55
+ # Since we don't know total, if we hit the limit, we say truncated=maybe
56
+ if returned >= pack_limit:
57
+ # next offset is unknown/not supported by simple symbol search usually,
58
+ # but we follow the format. offset=returned is best guess if paginated.
59
+ lines.append(pack_truncated(returned, pack_limit, "maybe"))
60
+
61
+ return "\n".join(lines)
62
+
63
+ return mcp_response("search_symbols", build_pack, build_json)
mcp/tools/status.py ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Status tool for Local Search MCP Server.
4
+ """
5
+ from typing import Any, Dict, Optional
6
+ from mcp.tools._util import mcp_response, pack_header, pack_line, resolve_root_ids
7
+
8
+ try:
9
+ from app.db import LocalSearchDB
10
+ from app.indexer import Indexer
11
+ from app.config import Config
12
+ from app.registry import ServerRegistry
13
+ from mcp.telemetry import TelemetryLogger
14
+ except ImportError:
15
+ # Fallback for direct script execution
16
+ import sys
17
+ from pathlib import Path
18
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
19
+ from app.db import LocalSearchDB
20
+ from app.indexer import Indexer
21
+ from app.config import Config
22
+ from app.registry import ServerRegistry
23
+ from mcp.telemetry import TelemetryLogger
24
+
25
+
26
+ def execute_status(args: Dict[str, Any], indexer: Optional[Indexer], db: Optional[LocalSearchDB], cfg: Optional[Config], workspace_root: str, server_version: str, logger: Optional[TelemetryLogger] = None) -> Dict[str, Any]:
27
+ """Execute status tool."""
28
+ details = bool(args.get("details", False))
29
+
30
+ # 1. Gather status data
31
+ actual_http_port = None
32
+ try:
33
+ inst = ServerRegistry().get_instance(workspace_root)
34
+ if inst and inst.get("port"):
35
+ actual_http_port = int(inst.get("port"))
36
+ except Exception:
37
+ actual_http_port = None
38
+
39
+ config_http_port = cfg.http_api_port if cfg else 0
40
+ status_data = {
41
+ "index_ready": indexer.status.index_ready if indexer else False,
42
+ "last_scan_ts": indexer.status.last_scan_ts if indexer else 0,
43
+ "last_commit_ts": indexer.get_last_commit_ts() if indexer and hasattr(indexer, "get_last_commit_ts") else 0,
44
+ "scanned_files": indexer.status.scanned_files if indexer else 0,
45
+ "indexed_files": indexer.status.indexed_files if indexer else 0,
46
+ "errors": indexer.status.errors if indexer else 0,
47
+ "fts_enabled": db.fts_enabled if db else False,
48
+ "workspace_root": workspace_root,
49
+ "server_version": server_version,
50
+ "http_api_port": actual_http_port if actual_http_port is not None else config_http_port,
51
+ "http_api_port_config": config_http_port,
52
+ "indexer_mode": getattr(indexer, "indexer_mode", "auto") if indexer else "off",
53
+ }
54
+ if db and hasattr(db, "engine") and hasattr(db.engine, "status"):
55
+ try:
56
+ st = db.engine.status()
57
+ status_data.update({
58
+ "engine_mode": st.engine_mode,
59
+ "engine_ready": st.engine_ready,
60
+ "engine_version": st.engine_version,
61
+ "index_docs": st.doc_count,
62
+ "index_size_bytes": st.index_size_bytes,
63
+ "last_build_ts": st.last_build_ts,
64
+ "engine_reason": st.reason,
65
+ "engine_hint": st.hint,
66
+ "engine_mem_mb": getattr(st, "engine_mem_mb", 0),
67
+ "index_mem_mb": getattr(st, "index_mem_mb", 0),
68
+ "engine_threads": getattr(st, "engine_threads", 0),
69
+ })
70
+ except Exception:
71
+ status_data.update({
72
+ "engine_mode": "embedded",
73
+ "engine_ready": False,
74
+ })
75
+ if indexer and hasattr(indexer, "get_queue_depths"):
76
+ status_data["queue_depths"] = indexer.get_queue_depths()
77
+
78
+ if cfg:
79
+ status_data["config"] = {
80
+ "include_ext": cfg.include_ext,
81
+ "exclude_dirs": cfg.exclude_dirs,
82
+ "exclude_globs": getattr(cfg, "exclude_globs", []),
83
+ "max_file_bytes": cfg.max_file_bytes,
84
+ "http_api_port": cfg.http_api_port,
85
+ }
86
+
87
+ repo_stats = None
88
+ if details and db:
89
+ root_ids = resolve_root_ids(cfg.workspace_roots if cfg else [])
90
+ repo_stats = db.get_repo_stats(root_ids=root_ids)
91
+ status_data["repo_stats"] = repo_stats
92
+
93
+ if logger:
94
+ logger.log_telemetry(f"tool=status details={details} scanned={status_data['scanned_files']} indexed={status_data['indexed_files']}")
95
+
96
+ # --- JSON Builder ---
97
+ def build_json() -> Dict[str, Any]:
98
+ return status_data
99
+
100
+ # --- PACK1 Builder ---
101
+ def build_pack() -> str:
102
+ metrics = []
103
+
104
+ # Base status
105
+ for k, v in status_data.items():
106
+ if k in {"config", "repo_stats", "queue_depths"}:
107
+ continue
108
+ val = str(v).lower() if isinstance(v, bool) else str(v)
109
+ metrics.append((k, val))
110
+
111
+ # Config (if exists)
112
+ if "config" in status_data:
113
+ c = status_data["config"]
114
+ metrics.append(("cfg_include_ext", ",".join(c.get("include_ext", []))))
115
+ metrics.append(("cfg_max_file_bytes", str(c.get("max_file_bytes", 0))))
116
+
117
+ if "queue_depths" in status_data:
118
+ q = status_data["queue_depths"]
119
+ metrics.append(("queue_watcher", str(q.get("watcher", 0))))
120
+ metrics.append(("queue_db_writer", str(q.get("db_writer", 0))))
121
+ metrics.append(("queue_telemetry", str(q.get("telemetry", 0))))
122
+
123
+ # Repo stats (if exists)
124
+ if repo_stats:
125
+ for r_name, r_count in repo_stats.items():
126
+ metrics.append((f"repo_{r_name}", str(r_count)))
127
+
128
+ # Build lines
129
+ lines = [pack_header("status", {}, returned=len(metrics))]
130
+ for k, v in metrics:
131
+ lines.append(pack_line("m", kv={k: v}))
132
+
133
+ return "\n".join(lines)
134
+
135
+ return mcp_response("status", build_pack, build_json)
sari/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from deckard.version import __version__
sari/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from deckard.main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())