ravmem-cli 0.1.0__tar.gz → 1.0.0__tar.gz
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-0.1.0/ravmem_cli.egg-info → ravmem_cli-1.0.0}/PKG-INFO +6 -1
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/pyproject.toml +7 -1
- ravmem_cli-1.0.0/ravmem/cli/_common.py +41 -0
- ravmem_cli-1.0.0/ravmem/cli/_errors.py +102 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/analyze.py +32 -18
- ravmem_cli-1.0.0/ravmem/cli/audit.py +83 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/auth.py +96 -86
- ravmem_cli-1.0.0/ravmem/cli/browse.py +206 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/diff.py +24 -39
- ravmem_cli-1.0.0/ravmem/cli/doctor.py +119 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/impact.py +25 -48
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/info.py +18 -19
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/kg.py +305 -69
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/main.py +9 -2
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/remote.py +241 -28
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/search.py +23 -43
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/serve.py +27 -4
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/api.py +124 -38
- ravmem_cli-1.0.0/ravmem/knowledge/__init__.py +0 -0
- ravmem_cli-1.0.0/ravmem/knowledge/config.py +139 -0
- ravmem_cli-1.0.0/ravmem/knowledge/init.py +142 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0/ravmem_cli.egg-info}/PKG-INFO +6 -1
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/SOURCES.txt +9 -1
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/requires.txt +6 -0
- ravmem_cli-1.0.0/tests/test_smoke.py +144 -0
- ravmem_cli-0.1.0/ravmem/cli/browse.py +0 -144
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/LICENSE +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/README.md +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/__init__.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/config_cmd.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/git_local.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/__init__.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/config.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/config.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/config_loader.py +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/dependency_links.txt +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/entry_points.txt +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/top_level.txt +0 -0
- {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ravmem-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
|
|
5
5
|
Author-email: RavMem <harpreet.singh@ravmem.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -18,9 +18,14 @@ Requires-Dist: typer[all]>=0.12.0
|
|
|
18
18
|
Requires-Dist: httpx>=0.27.0
|
|
19
19
|
Requires-Dist: pyyaml>=6.0
|
|
20
20
|
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Provides-Extra: serve
|
|
24
|
+
Requires-Dist: ravmem-server>=0.2.2; extra == "serve"
|
|
21
25
|
Provides-Extra: dev
|
|
22
26
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
23
27
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
|
|
24
29
|
Dynamic: license-file
|
|
25
30
|
|
|
26
31
|
# ravmem-cli
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ravmem-cli"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "RavMem CLI — lightweight client for the RavMem code knowledge graph server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,15 +26,21 @@ dependencies = [
|
|
|
26
26
|
"httpx>=0.27.0",
|
|
27
27
|
"pyyaml>=6.0",
|
|
28
28
|
"pydantic>=2.0",
|
|
29
|
+
"uvicorn>=0.27.0",
|
|
30
|
+
"rich>=13.0",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
32
34
|
Homepage = "https://www.ravmem.com"
|
|
33
35
|
|
|
34
36
|
[project.optional-dependencies]
|
|
37
|
+
serve = [
|
|
38
|
+
"ravmem-server>=0.2.2",
|
|
39
|
+
]
|
|
35
40
|
dev = [
|
|
36
41
|
"pytest>=8.0.0",
|
|
37
42
|
"pytest-asyncio>=0.23.0",
|
|
43
|
+
"pytest-httpx>=0.30.0",
|
|
38
44
|
]
|
|
39
45
|
|
|
40
46
|
[project.scripts]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Shared helpers for ravmem CLI commands."""
|
|
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: Optional[str]) -> 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: Optional[str]):
|
|
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
|
+
"No server configured. Run: ravmem remote config --url <URL>",
|
|
38
|
+
err=True,
|
|
39
|
+
)
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
return RavMemClient(base_url=url, api_key=api_key or None)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Shared CLI error-handling utilities.
|
|
2
|
+
|
|
3
|
+
All CLI modules import from here so every command surfaces the same
|
|
4
|
+
clean, human-readable messages instead of raw Python tracebacks.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _extract_detail(exc) -> str:
|
|
12
|
+
"""Pull the best human-readable string out of an httpx HTTP error body."""
|
|
13
|
+
try:
|
|
14
|
+
body = exc.response.json()
|
|
15
|
+
if isinstance(body, dict):
|
|
16
|
+
detail = body.get("detail") or body.get("message") or body.get("error")
|
|
17
|
+
if isinstance(detail, list):
|
|
18
|
+
# FastAPI validation error list - join the loc + msg pairs
|
|
19
|
+
parts = []
|
|
20
|
+
for item in detail:
|
|
21
|
+
loc = " -> ".join(str(x) for x in item.get("loc", []))
|
|
22
|
+
msg = item.get("msg", "")
|
|
23
|
+
parts.append(f"{loc}: {msg}" if loc else msg)
|
|
24
|
+
return "; ".join(parts)
|
|
25
|
+
if detail:
|
|
26
|
+
return str(detail)
|
|
27
|
+
return exc.response.text or str(exc)
|
|
28
|
+
except Exception:
|
|
29
|
+
return exc.response.text or str(exc)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def handle_error(exc: Exception, server_url: str = "") -> None:
|
|
33
|
+
"""Print a clean, user-friendly error message to stderr.
|
|
34
|
+
|
|
35
|
+
Does NOT raise - callers are responsible for raising typer.Exit(1) after.
|
|
36
|
+
"""
|
|
37
|
+
import httpx
|
|
38
|
+
|
|
39
|
+
if isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout)):
|
|
40
|
+
target = server_url
|
|
41
|
+
if not target:
|
|
42
|
+
try:
|
|
43
|
+
target = str(exc.request.url) if exc.request else ""
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
target = target or "the server"
|
|
47
|
+
typer.echo(
|
|
48
|
+
f"Error: Cannot connect to {target}.\n"
|
|
49
|
+
" Is the server running? Check: ravmem health",
|
|
50
|
+
err=True,
|
|
51
|
+
)
|
|
52
|
+
elif isinstance(exc, httpx.TimeoutException):
|
|
53
|
+
typer.echo(
|
|
54
|
+
"Error: Request timed out. The server may be overloaded or unreachable.",
|
|
55
|
+
err=True,
|
|
56
|
+
)
|
|
57
|
+
elif isinstance(exc, httpx.RemoteProtocolError):
|
|
58
|
+
typer.echo(
|
|
59
|
+
f"Error: Invalid response from server - {exc}",
|
|
60
|
+
err=True,
|
|
61
|
+
)
|
|
62
|
+
elif isinstance(exc, httpx.HTTPStatusError):
|
|
63
|
+
code = exc.response.status_code
|
|
64
|
+
detail = _extract_detail(exc)
|
|
65
|
+
if code == 400:
|
|
66
|
+
typer.echo(f"Error: Bad request - {detail}", err=True)
|
|
67
|
+
elif code == 401:
|
|
68
|
+
typer.echo(
|
|
69
|
+
"Error: Not authenticated.\n"
|
|
70
|
+
" Set an API token with: ravmem remote config --api-key <TOKEN>\n"
|
|
71
|
+
" Or log in with: ravmem auth login",
|
|
72
|
+
err=True,
|
|
73
|
+
)
|
|
74
|
+
elif code == 403:
|
|
75
|
+
typer.echo(
|
|
76
|
+
"Error: Permission denied - your token does not have the required permission.\n"
|
|
77
|
+
" Ask an admin to grant the necessary role.",
|
|
78
|
+
err=True,
|
|
79
|
+
)
|
|
80
|
+
elif code == 404:
|
|
81
|
+
typer.echo(f"Error: Not found - {detail}", err=True)
|
|
82
|
+
elif code == 409:
|
|
83
|
+
typer.echo(f"Error: Conflict - {detail}", err=True)
|
|
84
|
+
elif code == 422:
|
|
85
|
+
typer.echo(f"Error: Invalid request parameters - {detail}", err=True)
|
|
86
|
+
elif code == 429:
|
|
87
|
+
typer.echo(
|
|
88
|
+
"Error: Rate limit exceeded - too many requests. Wait a moment and retry.",
|
|
89
|
+
err=True,
|
|
90
|
+
)
|
|
91
|
+
elif code == 500:
|
|
92
|
+
typer.echo(f"Error: Server error - {detail}", err=True)
|
|
93
|
+
elif code == 503:
|
|
94
|
+
typer.echo(
|
|
95
|
+
f"Error: Server unavailable - {detail}\n"
|
|
96
|
+
" ArcadeDB may be starting up. Check: ravmem health",
|
|
97
|
+
err=True,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
typer.echo(f"Error {code}: {detail}", err=True)
|
|
101
|
+
else:
|
|
102
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
@@ -8,6 +8,8 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
+
from ravmem.cli._common import get_client as _get_client_shared
|
|
12
|
+
from ravmem.client.api import handle_api_error
|
|
11
13
|
from ravmem.config import DEFAULT_LANGS
|
|
12
14
|
|
|
13
15
|
# Extensions whose content is never useful for the code-graph pipeline.
|
|
@@ -38,23 +40,7 @@ _CLI_MAX_FILE_BYTES = 5 * 1024 * 1024
|
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def _get_client(remote: str | None):
|
|
41
|
-
|
|
42
|
-
from ravmem.client.config import load_config
|
|
43
|
-
|
|
44
|
-
url = remote
|
|
45
|
-
api_key = None
|
|
46
|
-
if not url:
|
|
47
|
-
cfg = load_config()
|
|
48
|
-
url = cfg.get("server_url")
|
|
49
|
-
api_key = cfg.get("api_key")
|
|
50
|
-
if not url:
|
|
51
|
-
typer.echo(
|
|
52
|
-
"Error: No server configured. "
|
|
53
|
-
"Run `ravmem remote config --url <URL>` or pass --remote <URL>.",
|
|
54
|
-
err=True,
|
|
55
|
-
)
|
|
56
|
-
raise typer.Exit(1)
|
|
57
|
-
return RavMemClient(base_url=url, api_key=api_key or None)
|
|
43
|
+
return _get_client_shared(remote)
|
|
58
44
|
|
|
59
45
|
|
|
60
46
|
def _print_job_result(job: dict) -> None:
|
|
@@ -395,7 +381,35 @@ def analyze_cmd(
|
|
|
395
381
|
"Use when you know the workspace is already up to date.",
|
|
396
382
|
),
|
|
397
383
|
) -> None:
|
|
398
|
-
"""Index a repository
|
|
384
|
+
"""Index a repository (auto-detects mode from the project registration).
|
|
385
|
+
|
|
386
|
+
\b
|
|
387
|
+
Register your project first with `ravmem remote add`, then run this command.
|
|
388
|
+
|
|
389
|
+
\b
|
|
390
|
+
Mode behaviour:
|
|
391
|
+
upload — CLI reads committed files locally and pushes them to the server.
|
|
392
|
+
The project must be registered without --git-url and without --mount.
|
|
393
|
+
Only committed files are indexed; commit before running.
|
|
394
|
+
clone — Server pulls from the git remote it cloned at registration.
|
|
395
|
+
No local files are uploaded. Use `ravmem remote clone` for first run.
|
|
396
|
+
mount — Server reads directly from the mounted path. No upload needed.
|
|
397
|
+
|
|
398
|
+
\b
|
|
399
|
+
Quick-start examples:
|
|
400
|
+
# Upload mode (local repo → server)
|
|
401
|
+
ravmem remote add /my/repo --name myproject
|
|
402
|
+
ravmem analyze /my/repo --project myproject --wait
|
|
403
|
+
|
|
404
|
+
# Clone mode (server pulls from GitHub)
|
|
405
|
+
ravmem remote add --git-url https://github.com/org/repo --name myproject
|
|
406
|
+
ravmem remote clone --project myproject --wait
|
|
407
|
+
ravmem analyze --project myproject --incremental --wait # subsequent runs
|
|
408
|
+
|
|
409
|
+
# Mount mode (repo already on the server filesystem)
|
|
410
|
+
ravmem remote add /server/path/repo --name myproject --mount
|
|
411
|
+
ravmem analyze --project myproject --wait
|
|
412
|
+
"""
|
|
399
413
|
|
|
400
414
|
repo_path = repo.resolve()
|
|
401
415
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""ravmem audit — audit log management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ravmem.cli._common import get_client
|
|
10
|
+
from ravmem.cli._errors import handle_error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def audit_list_cmd(
|
|
14
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Max entries to return"),
|
|
15
|
+
user: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user ID"),
|
|
16
|
+
action: Optional[str] = typer.Option(None, "--action", "-a", help="Filter by action name"),
|
|
17
|
+
remote: Optional[str] = typer.Option(None, "--remote", help="Server URL override"),
|
|
18
|
+
output_json: bool = typer.Option(False, "--json", help="Raw JSON output"),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""List audit log entries (admin only)."""
|
|
21
|
+
import json
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich import box
|
|
25
|
+
|
|
26
|
+
client = get_client(remote)
|
|
27
|
+
try:
|
|
28
|
+
entries = client.list_audit(limit=limit, user_id=user, action=action)
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
handle_error(exc)
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
finally:
|
|
33
|
+
client.close()
|
|
34
|
+
|
|
35
|
+
if output_json:
|
|
36
|
+
typer.echo(json.dumps(entries, indent=2, default=str))
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if not entries:
|
|
40
|
+
typer.echo("No audit entries found.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
t = Table(box=box.SIMPLE, show_header=True, header_style="bold",
|
|
45
|
+
title=f"Audit log ({len(entries)} entries)")
|
|
46
|
+
t.add_column("Time", width=19)
|
|
47
|
+
t.add_column("User", width=16)
|
|
48
|
+
t.add_column("Action", width=20)
|
|
49
|
+
t.add_column("Resource", width=30)
|
|
50
|
+
t.add_column("Result", width=8)
|
|
51
|
+
|
|
52
|
+
for e in entries:
|
|
53
|
+
result_color = "green" if e.get("result") == "ok" else "red"
|
|
54
|
+
t.add_row(
|
|
55
|
+
str(e.get("timestamp", ""))[:19],
|
|
56
|
+
str(e.get("user_id", ""))[:16],
|
|
57
|
+
str(e.get("action", ""))[:20],
|
|
58
|
+
str(e.get("resource", ""))[:30],
|
|
59
|
+
f"[{result_color}]{e.get('result', '?')}[/{result_color}]",
|
|
60
|
+
)
|
|
61
|
+
console.print(t)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def audit_rotate_cmd(
|
|
65
|
+
retain_days: int = typer.Option(90, "--retain-days", help="Keep entries newer than N days"),
|
|
66
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
67
|
+
remote: Optional[str] = typer.Option(None, "--remote", help="Server URL override"),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Rotate audit logs, discarding entries older than --retain-days (admin only)."""
|
|
70
|
+
if not yes:
|
|
71
|
+
typer.confirm(f"Rotate audit log (keep last {retain_days} days)?", abort=True)
|
|
72
|
+
|
|
73
|
+
client = get_client(remote)
|
|
74
|
+
try:
|
|
75
|
+
result = client.rotate_audit()
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
handle_error(exc)
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
finally:
|
|
80
|
+
client.close()
|
|
81
|
+
|
|
82
|
+
removed = result.get("removed", result.get("deleted", "?"))
|
|
83
|
+
typer.echo(f"Audit log rotated. Entries removed: {removed}")
|