ravmem-cli 0.1.0__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.
- ravmem/cli/__init__.py +0 -0
- ravmem/cli/analyze.py +484 -0
- ravmem/cli/auth.py +675 -0
- ravmem/cli/browse.py +144 -0
- ravmem/cli/config_cmd.py +69 -0
- ravmem/cli/diff.py +122 -0
- ravmem/cli/git_local.py +131 -0
- ravmem/cli/impact.py +106 -0
- ravmem/cli/info.py +82 -0
- ravmem/cli/kg.py +787 -0
- ravmem/cli/main.py +96 -0
- ravmem/cli/remote.py +549 -0
- ravmem/cli/search.py +91 -0
- ravmem/cli/serve.py +41 -0
- ravmem/client/__init__.py +3 -0
- ravmem/client/api.py +279 -0
- ravmem/client/config.py +52 -0
- ravmem/config.py +103 -0
- ravmem/config_loader.py +247 -0
- ravmem_cli-0.1.0.dist-info/METADATA +62 -0
- ravmem_cli-0.1.0.dist-info/RECORD +25 -0
- ravmem_cli-0.1.0.dist-info/WHEEL +5 -0
- ravmem_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ravmem_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ravmem_cli-0.1.0.dist-info/top_level.txt +1 -0
ravmem/cli/search.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""ravmem search — hybrid symbol search via the remote server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _detect_branch(repo_path: Path, explicit: str | None) -> str:
|
|
13
|
+
if explicit:
|
|
14
|
+
return explicit
|
|
15
|
+
try:
|
|
16
|
+
out = subprocess.run(
|
|
17
|
+
["git", "-C", str(repo_path), "rev-parse", "--abbrev-ref", "HEAD"],
|
|
18
|
+
capture_output=True, text=True, check=True,
|
|
19
|
+
).stdout.strip()
|
|
20
|
+
return out if out and out != "HEAD" else "main"
|
|
21
|
+
except Exception:
|
|
22
|
+
return "main"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_client(remote: str | None):
|
|
26
|
+
from ravmem.client.api import RavMemClient
|
|
27
|
+
from ravmem.client.config import load_config
|
|
28
|
+
|
|
29
|
+
url = remote
|
|
30
|
+
api_key = None
|
|
31
|
+
if not url:
|
|
32
|
+
cfg = load_config()
|
|
33
|
+
url = cfg.get("server_url")
|
|
34
|
+
api_key = cfg.get("api_key")
|
|
35
|
+
if not url:
|
|
36
|
+
typer.echo(
|
|
37
|
+
"Error: No server configured. "
|
|
38
|
+
"Run `ravmem remote config --url <URL>` or pass --remote <URL>.",
|
|
39
|
+
err=True,
|
|
40
|
+
)
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
return RavMemClient(base_url=url, api_key=api_key or None)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def search_cmd(
|
|
46
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
47
|
+
repo: Path = typer.Option(Path("."), "--repo", "-r", help="Path to the repository (used for branch auto-detection)"),
|
|
48
|
+
limit: int = typer.Option(10, "--limit", "-n", help="Number of results"),
|
|
49
|
+
remote: Optional[str] = typer.Option(None, "--remote", help="Server URL (overrides config)"),
|
|
50
|
+
project: Optional[str] = typer.Option(None, "--project", help="Project ID on the server"),
|
|
51
|
+
branch: Optional[str] = typer.Option(None, "--branch", help="Branch name (auto-detected from git if omitted)"),
|
|
52
|
+
output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Search symbols using hybrid BM25 + semantic search."""
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
repo_path = repo.resolve()
|
|
58
|
+
branch_name = _detect_branch(repo_path, branch)
|
|
59
|
+
|
|
60
|
+
if not project:
|
|
61
|
+
typer.echo("--project is required. Use `ravmem remote add` to register the repo first.", err=True)
|
|
62
|
+
raise typer.Exit(1)
|
|
63
|
+
|
|
64
|
+
client = _get_client(remote)
|
|
65
|
+
try:
|
|
66
|
+
results = client.search(project, branch_name, query, limit=limit)
|
|
67
|
+
finally:
|
|
68
|
+
client.close()
|
|
69
|
+
|
|
70
|
+
if output_json:
|
|
71
|
+
typer.echo(json.dumps(results, default=str))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if not results:
|
|
75
|
+
typer.echo("No results found.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
typer.echo(f"\nResults for '{query}' on branch '{branch_name}':\n")
|
|
79
|
+
for i, r in enumerate(results, 1):
|
|
80
|
+
sym = r.get("symbol", {})
|
|
81
|
+
name = sym.get("name", "")
|
|
82
|
+
kind = sym.get("kind", "")
|
|
83
|
+
file = sym.get("file", "")
|
|
84
|
+
line = sym.get("line_start", "")
|
|
85
|
+
score = r.get("score", 0.0)
|
|
86
|
+
location = f"{Path(file).name}:{line}" if file else ""
|
|
87
|
+
match_type = r.get("match_type", "hybrid")
|
|
88
|
+
typer.echo(
|
|
89
|
+
f" {i:2}. [{match_type:8}] {typer.style(name, bold=True)} "
|
|
90
|
+
f"({kind}) {location} score={score:.4f}"
|
|
91
|
+
)
|
ravmem/cli/serve.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""ravmem serve — start the FastAPI server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ravmem.config import DEFAULT_HOST, DEFAULT_PORT, SERVER_DATA_DIR
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def serve_cmd(
|
|
14
|
+
repos_dir: Path = typer.Option(
|
|
15
|
+
SERVER_DATA_DIR / "repos",
|
|
16
|
+
"--repos-dir",
|
|
17
|
+
help="Directory containing indexed repositories",
|
|
18
|
+
),
|
|
19
|
+
host: str = typer.Option(DEFAULT_HOST, "--host", help="Bind host"),
|
|
20
|
+
port: int = typer.Option(DEFAULT_PORT, "--port", "-p", help="Bind port"),
|
|
21
|
+
reload: bool = typer.Option(False, "--reload", help="Auto-reload on code changes (dev)"),
|
|
22
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging"),
|
|
23
|
+
config: Optional[str] = typer.Option(None, "--config", help="Path to ravmem.yaml config file"),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Start the RavMem API server (REST + MCP + WebSocket)."""
|
|
26
|
+
from ravmem.cli.main import setup_logging
|
|
27
|
+
import uvicorn
|
|
28
|
+
from ravmem.config_loader import reload_config
|
|
29
|
+
from ravmem.server.app import create_app
|
|
30
|
+
|
|
31
|
+
# Load config before anything else so all components see it
|
|
32
|
+
reload_config(config)
|
|
33
|
+
|
|
34
|
+
setup_logging(verbose)
|
|
35
|
+
repos_path = repos_dir.resolve()
|
|
36
|
+
typer.echo(f"Starting RavMem server on http://{host}:{port}")
|
|
37
|
+
typer.echo(f"Repos directory: {repos_path}")
|
|
38
|
+
typer.echo(f"MCP endpoint: http://{host}:{port}/mcp")
|
|
39
|
+
|
|
40
|
+
app = create_app(repos_dir=repos_path)
|
|
41
|
+
uvicorn.run(app, host=host, port=port, reload=reload)
|
ravmem/client/api.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Synchronous HTTP client for a remote RavMem server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RavMemClient:
|
|
11
|
+
"""Thin wrapper around the RavMem REST API."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str,
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
timeout: float = 30.0,
|
|
18
|
+
) -> None:
|
|
19
|
+
headers: dict[str, str] = {}
|
|
20
|
+
if api_key:
|
|
21
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=base_url, headers=headers, timeout=timeout
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
# Lifecycle
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def close(self) -> None:
|
|
31
|
+
self._client.close()
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def __exit__(self, *exc):
|
|
37
|
+
self.close()
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Internal helper
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def _request(self, method: str, path: str, **kwargs) -> dict | list:
|
|
44
|
+
resp = self._client.request(method, path, **kwargs)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
return resp.json()
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Projects
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def create_project(
|
|
53
|
+
self,
|
|
54
|
+
git_url: str | None = None,
|
|
55
|
+
git_username: str | None = None,
|
|
56
|
+
git_token: str | None = None,
|
|
57
|
+
repo_path: str | None = None,
|
|
58
|
+
mount: bool = False,
|
|
59
|
+
name: str | None = None,
|
|
60
|
+
default_branch: str = "main",
|
|
61
|
+
langs: list[str] | None = None,
|
|
62
|
+
) -> dict:
|
|
63
|
+
body: dict = {"default_branch": default_branch, "mount": mount}
|
|
64
|
+
if git_url is not None:
|
|
65
|
+
body["git_url"] = git_url
|
|
66
|
+
if git_username is not None:
|
|
67
|
+
body["git_username"] = git_username
|
|
68
|
+
if git_token is not None:
|
|
69
|
+
body["git_token"] = git_token
|
|
70
|
+
if repo_path is not None:
|
|
71
|
+
body["repo_path"] = repo_path
|
|
72
|
+
if name is not None:
|
|
73
|
+
body["name"] = name
|
|
74
|
+
if langs is not None:
|
|
75
|
+
body["langs"] = langs
|
|
76
|
+
return self._request("POST", "/api/projects", json=body)
|
|
77
|
+
|
|
78
|
+
def list_projects(self) -> list[dict]:
|
|
79
|
+
return self._request("GET", "/api/projects")
|
|
80
|
+
|
|
81
|
+
def get_project(self, project_id: str) -> dict:
|
|
82
|
+
return self._request("GET", f"/api/projects/{project_id}")
|
|
83
|
+
|
|
84
|
+
def delete_project(self, project_id: str) -> dict:
|
|
85
|
+
return self._request("DELETE", f"/api/projects/{project_id}")
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Branches
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def sync_workspace(self, project_id: str) -> dict:
|
|
92
|
+
"""Fetch the latest remote commits into the server-side workspace.
|
|
93
|
+
|
|
94
|
+
Returns ``{synced, fetched_branches, fetched_commits, workspace_head, duration_ms}``.
|
|
95
|
+
"""
|
|
96
|
+
return self._request("POST", f"/api/projects/{project_id}/workspace/sync")
|
|
97
|
+
|
|
98
|
+
def reset_workspace(self, project_id: str) -> dict:
|
|
99
|
+
"""Delete the server-side workspace and clear all branch index records.
|
|
100
|
+
|
|
101
|
+
Returns ``{reset: True, removed_workspace: bool, branches_cleared: int}``.
|
|
102
|
+
"""
|
|
103
|
+
return self._request("POST", f"/api/projects/{project_id}/workspace/reset")
|
|
104
|
+
|
|
105
|
+
def set_credentials(self, project_id: str, git_username: str, git_token: str) -> dict:
|
|
106
|
+
"""Store or replace git credentials for a clone-mode project.
|
|
107
|
+
|
|
108
|
+
Returns ``{stored: True}``.
|
|
109
|
+
"""
|
|
110
|
+
return self._request(
|
|
111
|
+
"POST",
|
|
112
|
+
f"/api/projects/{project_id}/credentials",
|
|
113
|
+
json={"git_username": git_username, "git_token": git_token},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def delete_credentials(self, project_id: str) -> dict:
|
|
117
|
+
"""Remove stored git credentials for a clone-mode project.
|
|
118
|
+
|
|
119
|
+
Returns ``{deleted: bool}``.
|
|
120
|
+
"""
|
|
121
|
+
return self._request("DELETE", f"/api/projects/{project_id}/credentials")
|
|
122
|
+
|
|
123
|
+
def clone_project(self, project_id: str, branch: str | None = None) -> dict:
|
|
124
|
+
"""Trigger initial clone + index for a clone-mode project.
|
|
125
|
+
|
|
126
|
+
Returns ``{"job_id": N}``.
|
|
127
|
+
"""
|
|
128
|
+
params: dict = {}
|
|
129
|
+
if branch is not None:
|
|
130
|
+
params["branch"] = branch
|
|
131
|
+
return self._request(
|
|
132
|
+
"POST", f"/api/projects/{project_id}/index/clone", params=params
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def upload_batch(
|
|
136
|
+
self,
|
|
137
|
+
project_id: str,
|
|
138
|
+
branch: str,
|
|
139
|
+
batch_index: int,
|
|
140
|
+
batch_total: int,
|
|
141
|
+
files: list[dict],
|
|
142
|
+
parent_branch: str | None = None,
|
|
143
|
+
to_commit: str = "",
|
|
144
|
+
from_commit: str = "",
|
|
145
|
+
mode: str = "full",
|
|
146
|
+
deleted_paths: list[str] | None = None,
|
|
147
|
+
langs: list[str] | None = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""POST one file batch to POST /api/projects/{id}/index.
|
|
150
|
+
|
|
151
|
+
Returns ``{"pending": True, "batch_index": N}`` for intermediate batches
|
|
152
|
+
and ``{"job_id": N}`` for the final batch.
|
|
153
|
+
"""
|
|
154
|
+
body: dict = {
|
|
155
|
+
"branch": branch,
|
|
156
|
+
"to_commit": to_commit,
|
|
157
|
+
"from_commit": from_commit,
|
|
158
|
+
"mode": mode,
|
|
159
|
+
"batch_index": batch_index,
|
|
160
|
+
"batch_total": batch_total,
|
|
161
|
+
"files": files,
|
|
162
|
+
"deleted_paths": deleted_paths or [],
|
|
163
|
+
}
|
|
164
|
+
if parent_branch is not None:
|
|
165
|
+
body["parent_branch"] = parent_branch
|
|
166
|
+
if langs is not None:
|
|
167
|
+
body["langs"] = langs
|
|
168
|
+
return self._request("POST", f"/api/projects/{project_id}/index", json=body)
|
|
169
|
+
|
|
170
|
+
def index_branch(
|
|
171
|
+
self, project_id: str, branch: str, mode: str = "auto",
|
|
172
|
+
langs: list[str] | None = None,
|
|
173
|
+
no_auto_sync: bool = False,
|
|
174
|
+
) -> dict:
|
|
175
|
+
body: dict = {"mode": mode}
|
|
176
|
+
if langs is not None:
|
|
177
|
+
body["langs"] = langs
|
|
178
|
+
if no_auto_sync:
|
|
179
|
+
body["no_auto_sync"] = True
|
|
180
|
+
return self._request(
|
|
181
|
+
"POST",
|
|
182
|
+
f"/api/projects/{project_id}/branches/{branch}/index",
|
|
183
|
+
json=body,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def list_branches(self, project_id: str) -> list[dict]:
|
|
187
|
+
return self._request("GET", f"/api/projects/{project_id}/branches")
|
|
188
|
+
|
|
189
|
+
def get_branch_status(self, project_id: str, branch: str) -> dict:
|
|
190
|
+
return self._request(
|
|
191
|
+
"GET", f"/api/projects/{project_id}/branches/{branch}/status"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def delete_branch_index(self, project_id: str, branch: str) -> dict:
|
|
195
|
+
return self._request(
|
|
196
|
+
"DELETE", f"/api/projects/{project_id}/branches/{branch}/index"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def get_divergence(self, project_id: str, branch: str) -> dict:
|
|
200
|
+
"""Pre-flight check: returns changed_files, total_files, ratio, recommend, force_push."""
|
|
201
|
+
return self._request(
|
|
202
|
+
"GET", f"/api/projects/{project_id}/branches/{branch}/divergence"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def update_project(self, project_id: str, default_branch: str) -> dict:
|
|
206
|
+
"""Update project metadata (e.g. change the default/parent branch)."""
|
|
207
|
+
return self._request(
|
|
208
|
+
"PATCH", f"/api/projects/{project_id}", json={"default_branch": default_branch}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Queries
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def search(
|
|
216
|
+
self,
|
|
217
|
+
project_id: str,
|
|
218
|
+
branch: str,
|
|
219
|
+
query: str,
|
|
220
|
+
limit: int = 20,
|
|
221
|
+
) -> list[dict]:
|
|
222
|
+
return self._request(
|
|
223
|
+
"GET",
|
|
224
|
+
f"/api/projects/{project_id}/branches/{branch}/search",
|
|
225
|
+
params={"q": query, "limit": limit},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def impact(
|
|
229
|
+
self,
|
|
230
|
+
project_id: str,
|
|
231
|
+
branch: str,
|
|
232
|
+
symbol_id: str,
|
|
233
|
+
depth: int = 3,
|
|
234
|
+
) -> dict:
|
|
235
|
+
return self._request(
|
|
236
|
+
"GET",
|
|
237
|
+
f"/api/projects/{project_id}/branches/{branch}/impact/{symbol_id}",
|
|
238
|
+
params={"depth": depth},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def get_symbol(
|
|
242
|
+
self, project_id: str, branch: str, symbol_id: str
|
|
243
|
+
) -> dict:
|
|
244
|
+
return self._request(
|
|
245
|
+
"GET",
|
|
246
|
+
f"/api/projects/{project_id}/branches/{branch}/symbols/{symbol_id}",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def get_clusters(self, project_id: str, branch: str) -> list[dict]:
|
|
250
|
+
return self._request(
|
|
251
|
+
"GET",
|
|
252
|
+
f"/api/projects/{project_id}/branches/{branch}/clusters",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
# Jobs
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def get_job(self, job_id: int) -> dict:
|
|
260
|
+
return self._request("GET", f"/api/jobs/{job_id}")
|
|
261
|
+
|
|
262
|
+
def poll_job(
|
|
263
|
+
self,
|
|
264
|
+
job_id: int,
|
|
265
|
+
interval: float = 2.0,
|
|
266
|
+
timeout: float = 600.0,
|
|
267
|
+
) -> dict:
|
|
268
|
+
"""Block until the job reaches a terminal state or *timeout* elapses."""
|
|
269
|
+
deadline = time.monotonic() + timeout
|
|
270
|
+
while True:
|
|
271
|
+
job = self.get_job(job_id)
|
|
272
|
+
status = job.get("status", "")
|
|
273
|
+
if status in ("done", "failed", "cancelled"):
|
|
274
|
+
return job
|
|
275
|
+
if time.monotonic() >= deadline:
|
|
276
|
+
raise TimeoutError(
|
|
277
|
+
f"Job {job_id} did not complete within {timeout}s"
|
|
278
|
+
)
|
|
279
|
+
time.sleep(interval)
|
ravmem/client/config.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Client configuration — manages ~/.ravmem/client.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ravmem.config import GLOBAL_DIR
|
|
9
|
+
|
|
10
|
+
CONFIG_PATH = GLOBAL_DIR / "client.json"
|
|
11
|
+
|
|
12
|
+
_DEFAULTS = {
|
|
13
|
+
"server_url": "",
|
|
14
|
+
"api_key": "",
|
|
15
|
+
"default_project": "",
|
|
16
|
+
"default_branch": "",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_config() -> dict:
|
|
21
|
+
"""Read client config from disk; returns defaults if file is missing."""
|
|
22
|
+
if not CONFIG_PATH.exists():
|
|
23
|
+
return dict(_DEFAULTS)
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
26
|
+
# Merge with defaults so new keys are always present
|
|
27
|
+
merged = dict(_DEFAULTS)
|
|
28
|
+
merged.update(data)
|
|
29
|
+
return merged
|
|
30
|
+
except (json.JSONDecodeError, OSError):
|
|
31
|
+
return dict(_DEFAULTS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_config(config: dict) -> None:
|
|
35
|
+
"""Write client config to disk, creating parent dirs if needed."""
|
|
36
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
CONFIG_PATH.write_text(
|
|
38
|
+
json.dumps(config, indent=2) + "\n", encoding="utf-8"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_client_from_config():
|
|
43
|
+
"""Return a RavMemClient from saved config, or None if not configured."""
|
|
44
|
+
from ravmem.client.api import RavMemClient
|
|
45
|
+
|
|
46
|
+
cfg = load_config()
|
|
47
|
+
if not cfg.get("server_url"):
|
|
48
|
+
return None
|
|
49
|
+
return RavMemClient(
|
|
50
|
+
base_url=cfg["server_url"],
|
|
51
|
+
api_key=cfg.get("api_key") or None,
|
|
52
|
+
)
|
ravmem/config.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Global constants and configuration defaults for RavMem."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Storage directory inside each indexed repo
|
|
7
|
+
INDEX_DIR = ".ravmem"
|
|
8
|
+
|
|
9
|
+
# Sub-paths within INDEX_DIR
|
|
10
|
+
GRAPH_SUBDIR = "graph"
|
|
11
|
+
VECTORS_SUBDIR = "vectors"
|
|
12
|
+
META_DB_NAME = "meta.db"
|
|
13
|
+
BM25_INDEX_NAME = "bm25.json"
|
|
14
|
+
CONFIG_SNAPSHOT_NAME = "config.json"
|
|
15
|
+
|
|
16
|
+
# Global user-level directory
|
|
17
|
+
GLOBAL_DIR = Path.home() / ".ravmem"
|
|
18
|
+
MODELS_DIR = GLOBAL_DIR / "models"
|
|
19
|
+
REGISTRY_DB = GLOBAL_DIR / "registry.db"
|
|
20
|
+
|
|
21
|
+
# Server data directory (centralised index storage for server mode)
|
|
22
|
+
SERVER_DATA_DIR = Path(os.environ.get("RAVMEM_DATA_DIR", str(GLOBAL_DIR / "server")))
|
|
23
|
+
|
|
24
|
+
# API key for server authentication (empty = auth disabled)
|
|
25
|
+
RAVMEM_API_KEY = os.environ.get("RAVMEM_API_KEY", "")
|
|
26
|
+
|
|
27
|
+
# Auth system config
|
|
28
|
+
RAVMEM_ADMIN_TOKEN = os.environ.get("RAVMEM_ADMIN_TOKEN", "")
|
|
29
|
+
RAVMEM_SYSTEM_DB = os.environ.get("RAVMEM_SYSTEM_DB", "ravmem_system")
|
|
30
|
+
RAVMEM_AUDIT_LOG_RETENTION_DAYS = int(os.environ.get("RAVMEM_AUDIT_LOG_RETENTION_DAYS", "90"))
|
|
31
|
+
|
|
32
|
+
# ArcadeDB connection config
|
|
33
|
+
ARCADEDB_HOST = os.environ.get("ARCADEDB_HOST", "localhost")
|
|
34
|
+
ARCADEDB_PORT = int(os.environ.get("ARCADEDB_PORT", "2480"))
|
|
35
|
+
ARCADEDB_PASSWORD = os.environ.get("ARCADEDB_PASSWORD", "arcadedb")
|
|
36
|
+
|
|
37
|
+
# Shared ArcadeDB database name (single DB for all branches; isolation via branch property)
|
|
38
|
+
ARCADE_DB_NAME = "codegraph"
|
|
39
|
+
|
|
40
|
+
# Embedding model
|
|
41
|
+
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
|
42
|
+
EMBEDDING_DIM = 384
|
|
43
|
+
|
|
44
|
+
# Server defaults
|
|
45
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
46
|
+
DEFAULT_PORT = 7432
|
|
47
|
+
|
|
48
|
+
# Indexing defaults
|
|
49
|
+
DEFAULT_LANGS = ["python", "typescript", "javascript", "go"]
|
|
50
|
+
MAX_FILE_SIZE_KB = 500 # skip files larger than this
|
|
51
|
+
|
|
52
|
+
# Clustering
|
|
53
|
+
CLUSTER_SEED = int(os.environ.get("RAVMEM_CLUSTER_SEED", "42"))
|
|
54
|
+
CLUSTER_RESOLUTION = float(os.environ.get("RAVMEM_CLUSTER_RESOLUTION", "1.0"))
|
|
55
|
+
CLUSTER_MAX_SIZE = int(os.environ.get("RAVMEM_CLUSTER_MAX_SIZE", "500"))
|
|
56
|
+
CLUSTER_SUBCLUSTER_THRESHOLD = int(os.environ.get("RAVMEM_CLUSTER_SUBCLUSTER_THRESHOLD", "200"))
|
|
57
|
+
|
|
58
|
+
# OIDC / SSO config (all optional; OIDC is disabled when OIDC_ISSUER_URL is not set)
|
|
59
|
+
OIDC_ENABLED = os.environ.get("RAVMEM_OIDC_ENABLED", "").lower() in ("1", "true", "yes")
|
|
60
|
+
OIDC_ISSUER_URL = os.environ.get("RAVMEM_OIDC_ISSUER_URL", "").rstrip("/")
|
|
61
|
+
OIDC_CLIENT_ID = os.environ.get("RAVMEM_OIDC_CLIENT_ID", "")
|
|
62
|
+
OIDC_CLIENT_SECRET = os.environ.get("RAVMEM_OIDC_CLIENT_SECRET", "")
|
|
63
|
+
OIDC_SCOPES = os.environ.get("RAVMEM_OIDC_SCOPES", "openid email profile")
|
|
64
|
+
|
|
65
|
+
# Language → file extension mapping
|
|
66
|
+
LANG_EXTENSIONS: dict[str, list[str]] = {
|
|
67
|
+
"python": [".py"],
|
|
68
|
+
"typescript": [".ts", ".tsx"],
|
|
69
|
+
"javascript": [".js", ".jsx"],
|
|
70
|
+
"go": [".go"],
|
|
71
|
+
"rust": [".rs"],
|
|
72
|
+
"java": [".java"],
|
|
73
|
+
"csharp": [".cs"],
|
|
74
|
+
"php": [".php"],
|
|
75
|
+
"ruby": [".rb"],
|
|
76
|
+
"kotlin": [".kt", ".kts"],
|
|
77
|
+
"c": [".c", ".h"],
|
|
78
|
+
"cpp": [".cpp", ".cc", ".cxx", ".hpp"],
|
|
79
|
+
"shell": [".sh", ".bash"],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
EXT_TO_LANG: dict[str, str] = {
|
|
83
|
+
ext: lang for lang, exts in LANG_EXTENSIONS.items() for ext in exts
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Default ignore patterns (similar to .gitignore)
|
|
87
|
+
DEFAULT_IGNORE_PATTERNS = [
|
|
88
|
+
".git",
|
|
89
|
+
".ravmem",
|
|
90
|
+
"__pycache__",
|
|
91
|
+
"node_modules",
|
|
92
|
+
".venv",
|
|
93
|
+
"venv",
|
|
94
|
+
"env",
|
|
95
|
+
"dist",
|
|
96
|
+
"build",
|
|
97
|
+
"*.egg-info",
|
|
98
|
+
".mypy_cache",
|
|
99
|
+
".pytest_cache",
|
|
100
|
+
".ruff_cache",
|
|
101
|
+
"*.min.js",
|
|
102
|
+
"*.bundle.js",
|
|
103
|
+
]
|