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.
Files changed (39) hide show
  1. {ravmem_cli-0.1.0/ravmem_cli.egg-info → ravmem_cli-1.0.0}/PKG-INFO +6 -1
  2. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/pyproject.toml +7 -1
  3. ravmem_cli-1.0.0/ravmem/cli/_common.py +41 -0
  4. ravmem_cli-1.0.0/ravmem/cli/_errors.py +102 -0
  5. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/analyze.py +32 -18
  6. ravmem_cli-1.0.0/ravmem/cli/audit.py +83 -0
  7. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/auth.py +96 -86
  8. ravmem_cli-1.0.0/ravmem/cli/browse.py +206 -0
  9. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/diff.py +24 -39
  10. ravmem_cli-1.0.0/ravmem/cli/doctor.py +119 -0
  11. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/impact.py +25 -48
  12. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/info.py +18 -19
  13. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/kg.py +305 -69
  14. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/main.py +9 -2
  15. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/remote.py +241 -28
  16. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/search.py +23 -43
  17. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/serve.py +27 -4
  18. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/api.py +124 -38
  19. ravmem_cli-1.0.0/ravmem/knowledge/__init__.py +0 -0
  20. ravmem_cli-1.0.0/ravmem/knowledge/config.py +139 -0
  21. ravmem_cli-1.0.0/ravmem/knowledge/init.py +142 -0
  22. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0/ravmem_cli.egg-info}/PKG-INFO +6 -1
  23. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/SOURCES.txt +9 -1
  24. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/requires.txt +6 -0
  25. ravmem_cli-1.0.0/tests/test_smoke.py +144 -0
  26. ravmem_cli-0.1.0/ravmem/cli/browse.py +0 -144
  27. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/LICENSE +0 -0
  28. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/README.md +0 -0
  29. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/__init__.py +0 -0
  30. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/config_cmd.py +0 -0
  31. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/cli/git_local.py +0 -0
  32. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/__init__.py +0 -0
  33. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/client/config.py +0 -0
  34. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/config.py +0 -0
  35. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem/config_loader.py +0 -0
  36. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/dependency_links.txt +0 -0
  37. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/entry_points.txt +0 -0
  38. {ravmem_cli-0.1.0 → ravmem_cli-1.0.0}/ravmem_cli.egg-info/top_level.txt +0 -0
  39. {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: 0.1.0
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 = "0.1.0"
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
- from ravmem.client.api import RavMemClient
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 — detects upload/clone/mount mode from the project registration."""
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}")