ravmem-cli 1.2.0__tar.gz → 1.3.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 (38) hide show
  1. {ravmem_cli-1.2.0/ravmem_cli.egg-info → ravmem_cli-1.3.0}/PKG-INFO +1 -1
  2. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/pyproject.toml +1 -1
  3. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/license.py +158 -76
  4. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/main.py +10 -0
  5. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0/ravmem_cli.egg-info}/PKG-INFO +1 -1
  6. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/LICENSE +0 -0
  7. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/README.md +0 -0
  8. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/__init__.py +0 -0
  9. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/_common.py +0 -0
  10. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/_errors.py +0 -0
  11. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/analyze.py +0 -0
  12. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/audit.py +0 -0
  13. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/auth.py +0 -0
  14. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/browse.py +0 -0
  15. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/config_cmd.py +0 -0
  16. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/diff.py +0 -0
  17. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/doctor.py +0 -0
  18. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/git_local.py +0 -0
  19. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/impact.py +0 -0
  20. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/info.py +0 -0
  21. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/kg.py +0 -0
  22. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/remote.py +0 -0
  23. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/search.py +0 -0
  24. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/serve.py +0 -0
  25. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/__init__.py +0 -0
  26. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/api.py +0 -0
  27. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/config.py +0 -0
  28. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/config.py +0 -0
  29. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/config_loader.py +0 -0
  30. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/kg_config.py +0 -0
  31. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/kg_init.py +0 -0
  32. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/SOURCES.txt +0 -0
  33. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/dependency_links.txt +0 -0
  34. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/entry_points.txt +0 -0
  35. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/requires.txt +0 -0
  36. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/top_level.txt +0 -0
  37. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/setup.cfg +0 -0
  38. {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ravmem-cli
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ravmem-cli"
7
- version = "1.2.0"
7
+ version = "1.3.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"
@@ -47,7 +47,7 @@ def _post_with_key_fallback(client, con: Console, path: str, body: Optional[dict
47
47
  try:
48
48
  return client.post(path, json=body)
49
49
  except RavMemAPIError as exc:
50
- if exc.status == 400 and "RAVMEM_LICENSE_KEY is not set" in (exc.detail or ""):
50
+ if exc.status == 400 and "RAVMEM_LICENSE_KEY is not set" in (exc.detail or "") and not body.get("license_key"):
51
51
  con.print("[yellow]Server has no license key on file — enter it to proceed.[/yellow]")
52
52
  key = typer.prompt("License key", hide_input=True).strip()
53
53
  if not key:
@@ -57,6 +57,37 @@ def _post_with_key_fallback(client, con: Console, path: str, body: Optional[dict
57
57
  raise
58
58
 
59
59
 
60
+ def _license_hint(detail: str) -> Optional[str]:
61
+ """Map a license-server refusal to the concrete next step for the operator."""
62
+ d = detail or ""
63
+ if "LICENSE_REVOKED" in d:
64
+ return ("This license was revoked. Rotate the key in the console (that un-revokes it and "
65
+ "issues a new key), then run: ravmem license activate <new-key>")
66
+ if "LICENSE_KEY_NOT_FOUND" in d or "License key not found" in d or "No account found" in d:
67
+ return ("The license server doesn't recognise this key. If it was rotated in the console, "
68
+ "run: ravmem license activate <new-key>")
69
+ if "ACTIVATION_LIMIT_REACHED" in d:
70
+ return ("Every seat on this license is in use — possibly by this same machine under a "
71
+ "previous key. Deactivate the old machine in the console, then retry.")
72
+ if "SUBSCRIPTION_SUSPENDED" in d:
73
+ return "The subscription is suspended. Resolve billing in the console, then run: ravmem license renew"
74
+ if "SUBSCRIPTION_EXPIRED" in d:
75
+ return "The subscription has expired. Renew it in the console, then run: ravmem license renew"
76
+ if "No license record found" in d:
77
+ return "Run: ravmem license activate <key>"
78
+ return None
79
+
80
+
81
+ def _fail(con: Console, prefix: str, exc: Exception, note: Optional[str] = None) -> None:
82
+ con.print(f"[red]{prefix}: {exc}[/red]")
83
+ if note:
84
+ con.print(f"[dim]{note}[/dim]")
85
+ hint = _license_hint(getattr(exc, "detail", "") or str(exc))
86
+ if hint:
87
+ con.print(f"[yellow]-> {hint}[/yellow]")
88
+ raise typer.Exit(1)
89
+
90
+
60
91
  # ---------------------------------------------------------------------------
61
92
  # ravmem license init
62
93
  # ---------------------------------------------------------------------------
@@ -146,19 +177,25 @@ def activate_cmd(
146
177
  # that is fine — we just continue to the entitlement step.
147
178
  con.print("Sealing license to this machine…")
148
179
  sealed_now = False
149
- try:
150
- client.post("/activate", json={"license_key": key})
151
- sealed_now = True
152
- con.print("[green]K3 sealed. Waiting for the server to load modules…[/green]")
153
- except RavMemAPIError as exc:
154
- if exc.status == 404:
155
- con.print("[dim]Server already activated — proceeding to entitlement init.[/dim]")
156
- else:
157
- con.print(f"[red]Activation failed: {exc}[/red]")
180
+ for attempt in range(3):
181
+ try:
182
+ client.post("/activate", json={"license_key": key})
183
+ sealed_now = True
184
+ con.print("[green]K3 sealed. Waiting for the server to load modules…[/green]")
185
+ break
186
+ except RavMemAPIError as exc:
187
+ if exc.status == 404:
188
+ con.print("[dim]Server already activated — proceeding to entitlement init.[/dim]")
189
+ break
190
+ _fail(con, "Activation failed", exc)
191
+ except Exception as exc:
192
+ # Transient: server still booting, or a network blip to it.
193
+ if attempt < 2:
194
+ time.sleep(3)
195
+ continue
196
+ con.print(f"[red]Could not reach the server: {exc}[/red]")
197
+ con.print("[yellow]-> Check it's running ('docker logs ravmem'), then retry.[/yellow]")
158
198
  raise typer.Exit(1)
159
- except Exception as exc:
160
- con.print(f"[red]Could not reach the server: {exc}[/red]")
161
- raise typer.Exit(1)
162
199
 
163
200
  # Phase 2 — wait for the re-exec'd full server to finish loading modules.
164
201
  if sealed_now:
@@ -181,44 +218,57 @@ def activate_cmd(
181
218
 
182
219
  # Phase 3 — store the signed entitlement JWT (idempotent: 409 = already done).
183
220
  con.print("Activating entitlements…")
184
- init_payload: dict = {"key_hash": key_hash}
221
+ init_payload: dict = {"key_hash": key_hash, "license_key": key}
185
222
  if server_url:
186
223
  init_payload["server_url"] = server_url
187
224
  try:
188
225
  result = client.post("/api/license/init", json=init_payload)
189
226
  except RavMemAPIError as exc:
190
- if exc.status == 409:
191
- # Already initialized — a 409 alone doesn't mean the stored token is
192
- # still usable (e.g. a container recreate can make it undecryptable).
193
- # Check token_integrity and repair if it isn't "ok" before declaring
194
- # success.
195
- con.print("[dim]License already initialized — verifying token integrity…[/dim]")
196
- try:
197
- status_data = client.get("/api/license/status")
198
- except RavMemAPIError as exc2:
199
- con.print(f"[red]Could not verify license status: {exc2}[/red]")
200
- raise typer.Exit(1)
227
+ if exc.status != 409:
228
+ _fail(con, "Entitlement init failed", exc)
229
+
230
+ # Already initialized. That alone doesn't mean everything is fine: the
231
+ # stored token may be broken (recreate), the license may be
232
+ # deactivated/revoked, or the key typed here may differ from the one
233
+ # this server holds (rotated key, or a typo). Only report "ready" when
234
+ # all three check out; otherwise re-fetch with the key given here —
235
+ # the license server validates it either way, and on refusal nothing
236
+ # stored locally is changed.
237
+ con.print("[dim]License already initialized — verifying…[/dim]")
238
+ try:
239
+ status_data = client.get("/api/license/status")
240
+ except RavMemAPIError as exc2:
241
+ _fail(con, "Could not verify license status", exc2)
201
242
 
202
- integrity = status_data.get("token_integrity")
203
- if integrity == "ok":
204
- con.print("[bold green]License already initialized — ravmem is ready.[/bold green]")
205
- return
243
+ integrity = status_data.get("token_integrity")
244
+ status = status_data.get("status")
245
+ stored_prefix = status_data.get("key_hash_prefix")
246
+ key_differs = bool(stored_prefix) and not key_hash.startswith(stored_prefix)
206
247
 
207
- con.print(f"[yellow]Stored token integrity is '{integrity}' — repairing…[/yellow]")
208
- try:
209
- # Already have the key in scope from this activate call — no
210
- # need to prompt again even if the server has nothing on file.
211
- repair_result = client.post("/api/license/repair", json={"license_key": key})
212
- except RavMemAPIError as exc3:
213
- con.print(f"[red]Repair failed: {exc3}[/red]")
214
- raise typer.Exit(1)
215
-
216
- con.print("\n[bold green]License repaired. ravmem is ready.[/bold green]")
217
- con.print(f" Status: {repair_result.get('status')}")
218
- con.print(f" Expires: {repair_result.get('expiry_ts')}")
248
+ if integrity == "ok" and status in ("ACTIVE", "GRACE_PERIOD") and not key_differs:
249
+ con.print("[bold green]License already initialized — ravmem is ready.[/bold green]")
219
250
  return
220
- con.print(f"[red]Entitlement init failed: {exc}[/red]")
221
- raise typer.Exit(1)
251
+
252
+ if key_differs:
253
+ con.print("[yellow]This key differs from the one this server is activated with — "
254
+ "switching to it…[/yellow]")
255
+ elif status not in ("ACTIVE", "GRACE_PERIOD"):
256
+ con.print(f"[yellow]License status is {status} — re-activating…[/yellow]")
257
+ else:
258
+ con.print(f"[yellow]Stored token integrity is '{integrity}' — repairing…[/yellow]")
259
+ try:
260
+ repair_result = client.post("/api/license/repair", json={"license_key": key})
261
+ except RavMemAPIError as exc3:
262
+ _fail(con, "Activation failed", exc3,
263
+ note="Your existing license on this server was left unchanged." if key_differs else None)
264
+
265
+ if repair_result.get("status") == "rebind_pending":
266
+ con.print(f"[yellow]{repair_result.get('message')}[/yellow]")
267
+ raise typer.Exit(1)
268
+ con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
269
+ con.print(f" Status: {repair_result.get('status')}")
270
+ con.print(f" Expires: {repair_result.get('expiry_ts')}")
271
+ return
222
272
 
223
273
  con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
224
274
  con.print(f" Status: {result.get('status')}")
@@ -382,6 +432,8 @@ def status_cmd(
382
432
  expiry_ts = data.get("expiry_ts")
383
433
  table.add_row("Expires", _fmt_ts(expiry_ts) + _days_until(expiry_ts))
384
434
  table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
435
+ if data.get("heartbeat_interval"):
436
+ table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
385
437
  table.add_row("Last server time", _fmt_ts(data.get("last_server_time")))
386
438
 
387
439
  grace_deadline = data.get("grace_deadline")
@@ -405,6 +457,8 @@ def status_cmd(
405
457
  )
406
458
  elif status == "GRACE_PERIOD":
407
459
  con.print("\n[yellow]⚠ License in grace period. Renew before the grace window closes.[/yellow]")
460
+ else:
461
+ _status_hint(con, status)
408
462
 
409
463
  if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED"):
410
464
  raise typer.Exit(1)
@@ -414,16 +468,31 @@ def status_cmd(
414
468
  # ravmem license renew
415
469
  # ---------------------------------------------------------------------------
416
470
 
471
+ _KEY_OPTION_HELP = (
472
+ "License key to use. Only needed if the key was rotated in the console "
473
+ "(this server is then switched onto it), or if the server has no key on file."
474
+ )
475
+
476
+
477
+ def _run_recovery(con: Console, path: str, key: Optional[str], failed: str) -> dict:
478
+ client = _get_client()
479
+ body = {"license_key": key.strip()} if key and key.strip() else {}
480
+ try:
481
+ return _post_with_key_fallback(client, con, path, body)
482
+ except RavMemAPIError as exc:
483
+ _fail(con, failed, exc)
484
+
485
+
417
486
  @app.command("renew")
418
- def renew_cmd() -> None:
487
+ def renew_cmd(
488
+ key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
489
+ ) -> None:
419
490
  """Re-activate an expired license against the live license server."""
420
491
  con = _console()
421
- client = _get_client()
422
492
  con.print("Renewing license…")
423
- try:
424
- result = _post_with_key_fallback(client, con, "/api/license/renew")
425
- except RavMemAPIError as exc:
426
- con.print(f"[red]Renewal failed: {exc}[/red]")
493
+ result = _run_recovery(con, "/api/license/renew", key, "Renewal failed")
494
+ if result.get("status") == "rebind_pending":
495
+ con.print(f"[yellow]{result.get('message')}[/yellow]")
427
496
  raise typer.Exit(1)
428
497
  con.print(f"[green]Renewed. Status: {result.get('status')} | Expiry: {result.get('expiry_ts')}[/green]")
429
498
 
@@ -433,20 +502,19 @@ def renew_cmd() -> None:
433
502
  # ---------------------------------------------------------------------------
434
503
 
435
504
  @app.command("repair")
436
- def repair_cmd() -> None:
437
- """Re-fetch and re-encrypt the JWT when the stored token is corrupted."""
505
+ def repair_cmd(
506
+ key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
507
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
508
+ ) -> None:
509
+ """Re-fetch a valid license token for this server (broken token, recreated
510
+ container, deactivated machine, or a key rotated in the console)."""
438
511
  con = _console()
439
- con.print(
440
- "[yellow]This will re-contact the license server using the stored key_hash "
441
- "and machine fingerprint. RAVMEM_LICENSE_KEY must be set on the server.[/yellow]"
442
- )
443
- typer.confirm("Continue?", abort=True)
444
-
445
- client = _get_client()
446
- try:
447
- result = _post_with_key_fallback(client, con, "/api/license/repair")
448
- except RavMemAPIError as exc:
449
- con.print(f"[red]Repair failed: {exc}[/red]")
512
+ if not yes:
513
+ con.print("[yellow]This re-contacts the license server and replaces this server's stored token.[/yellow]")
514
+ typer.confirm("Continue?", abort=True)
515
+ result = _run_recovery(con, "/api/license/repair", key, "Repair failed")
516
+ if result.get("status") == "rebind_pending":
517
+ con.print(f"[yellow]{result.get('message')}[/yellow]")
450
518
  raise typer.Exit(1)
451
519
  con.print(f"[green]Repair complete. Status: {result.get('status')}[/green]")
452
520
 
@@ -489,29 +557,43 @@ def check_cmd() -> None:
489
557
  table.add_row("Revision", str(data.get("revision", "—")))
490
558
  table.add_row("Expires", _fmt_ts(data.get("expiry_ts")))
491
559
  table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
560
+ if data.get("heartbeat_interval"):
561
+ table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
492
562
  grace_deadline = data.get("grace_deadline")
493
563
  if grace_deadline:
494
564
  table.add_row("[yellow]Grace deadline[/yellow]", f"[yellow]{_fmt_ts(grace_deadline)}[/yellow]")
495
565
  con.print(table)
566
+ _status_hint(con, status)
496
567
 
497
568
  if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED", "SUSPENDED"):
498
569
  raise typer.Exit(1)
499
570
 
500
571
 
572
+ def _status_hint(con: Console, status: str) -> None:
573
+ hints = {
574
+ "REVOKED": _license_hint("LICENSE_REVOKED"),
575
+ "REQUIRES_REACTIVATION": "Run: ravmem license repair (or: ravmem license activate <key>)",
576
+ "TAMPERED": "Run: ravmem license repair",
577
+ "DEACTIVATED": "This machine was deactivated. To keep it, run: ravmem license activate <key>",
578
+ "SUSPENDED": _license_hint("SUBSCRIPTION_SUSPENDED"),
579
+ "UNINITIALIZED": "Run: ravmem license activate <key>",
580
+ }
581
+ if hints.get(status):
582
+ con.print(f"[yellow]-> {hints[status]}[/yellow]")
583
+
584
+
501
585
  @app.command("rebind")
502
- def rebind_cmd() -> None:
503
- """Request a hardware fingerprint rebind after machine migration."""
586
+ def rebind_cmd(
587
+ key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
588
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
589
+ ) -> None:
590
+ """Move this license's seat onto this machine (after migration/recreate)."""
504
591
  con = _console()
505
- con.print(
506
- "[yellow]This submits a rebind request to the license server. "
507
- "Vendor approval is required before a new token is issued.[/yellow]"
508
- )
509
- typer.confirm("Continue?", abort=True)
510
-
511
- client = _get_client()
512
- try:
513
- result = _post_with_key_fallback(client, con, "/api/license/rebind")
514
- except RavMemAPIError as exc:
515
- con.print(f"[red]Rebind request failed: {exc}[/red]")
592
+ if not yes:
593
+ con.print("[yellow]This moves the license seat onto this machine's current identity.[/yellow]")
594
+ typer.confirm("Continue?", abort=True)
595
+ result = _run_recovery(con, "/api/license/rebind", key, "Rebind failed")
596
+ if result.get("status") == "rebind_pending":
597
+ con.print(f"[yellow]{result.get('message')}[/yellow]")
516
598
  raise typer.Exit(1)
517
- con.print(f"[green]{result.get('message', 'Rebind request submitted.')}[/green]")
599
+ con.print(f"[green]{result.get('message', 'Rebound.')}[/green]")
@@ -1,10 +1,20 @@
1
1
  """CLI entry point — registers all subcommands."""
2
2
 
3
3
  import logging
4
+ import sys
4
5
  from typing import Optional
5
6
 
6
7
  import typer
7
8
 
9
+ # Legacy Windows consoles (cp1252) can't encode the ⚠/…/— glyphs used in
10
+ # output; without this, e.g. `ravmem license status` crashed with a
11
+ # UnicodeEncodeError *after* printing, exiting non-zero on a healthy license.
12
+ for _stream in (sys.stdout, sys.stderr):
13
+ try:
14
+ _stream.reconfigure(errors="replace")
15
+ except Exception:
16
+ pass
17
+
8
18
 
9
19
  def setup_logging(verbose: bool = False) -> None:
10
20
  """Configure root logger based on verbosity or RAVMEM_LOG_LEVEL env var.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ravmem-cli
3
- Version: 1.2.0
3
+ Version: 1.3.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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes