sibyl-memory-cli 0.3.13__tar.gz → 0.3.15__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 (26) hide show
  1. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/PKG-INFO +1 -1
  2. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/pyproject.toml +1 -1
  3. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/cli.py +141 -0
  4. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/PKG-INFO +1 -1
  5. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/SOURCES.txt +2 -0
  6. sibyl_memory_cli-0.3.15/tests/test_memory_cmd_2026_06_16.py +55 -0
  7. sibyl_memory_cli-0.3.15/tests/test_status_stores_2026_06_11.py +70 -0
  8. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/README.md +0 -0
  9. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/setup.cfg +0 -0
  10. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/__init__.py +0 -0
  11. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/_aesthetic.py +0 -0
  12. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/_banner.py +0 -0
  13. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/migrate.py +0 -0
  14. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/setup.py +0 -0
  15. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
  16. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
  17. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
  18. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
  19. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_hermes_install_args.py +0 -0
  20. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_init_perms_guard.py +0 -0
  21. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_migrate.py +0 -0
  22. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_migrate_adversarial.py +0 -0
  23. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_migrate_force_2026_06_05.py +0 -0
  24. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_orchestrator.py +0 -0
  25. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_setup.py +0 -0
  26. {sibyl_memory_cli-0.3.13 → sibyl_memory_cli-0.3.15}/tests/test_wiring_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sibyl-memory-cli"
7
- version = "0.3.13"
7
+ version = "0.3.15"
8
8
  description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
9
9
  authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
10
10
  license = { text = "MIT" }
@@ -516,6 +516,55 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
516
516
 
517
517
  # ---- `sibyl status` ----------------------------------------------------
518
518
 
519
+ def _discover_stores(primary_db: Path) -> list[dict[str, Any]]:
520
+ """Enumerate every memory.db an agent on this machine might resolve.
521
+
522
+ Beta reports (VRTX 2026-06-11) showed split-brain storage: the SDK / CLI /
523
+ MCP default (``~/.sibyl-memory/memory.db``), the Hermes adapter
524
+ (``$HERMES_HOME/sibyl/memory.db``), per-profile DBs
525
+ (``$HERMES_HOME/sibyl/profiles/<p>/memory.db``), and an MCP
526
+ ``SIBYL_MEMORY_DB`` override can each hold a disjoint set of memories, so a
527
+ user switching entry points sees memory "vanish". This surfaces all of
528
+ them in one place. Read-only: it never creates or moves anything (path
529
+ unification is a separate, migration-gated change).
530
+
531
+ Returns one dict per DISTINCT existing store, resolved + deduped:
532
+ ``{"label", "path", "size"}``.
533
+ """
534
+ candidates: list[tuple[str, Path]] = [("default (SDK/CLI/MCP)", primary_db)]
535
+
536
+ hermes_home_env = os.environ.get("HERMES_HOME")
537
+ hermes_home = Path(hermes_home_env).expanduser() if hermes_home_env else (Path.home() / ".hermes")
538
+ candidates.append(("hermes adapter", hermes_home / "sibyl" / "memory.db"))
539
+ profiles_dir = hermes_home / "sibyl" / "profiles"
540
+ if profiles_dir.is_dir():
541
+ for prof in sorted(profiles_dir.iterdir()):
542
+ db = prof / "memory.db"
543
+ if db.exists():
544
+ candidates.append((f"hermes profile · {prof.name}", db))
545
+
546
+ mcp_override = os.environ.get("SIBYL_MEMORY_DB")
547
+ if mcp_override:
548
+ candidates.append(("MCP SIBYL_MEMORY_DB", Path(mcp_override).expanduser()))
549
+
550
+ seen: set[str] = set()
551
+ stores: list[dict[str, Any]] = []
552
+ for label, path in candidates:
553
+ try:
554
+ resolved = str(path.resolve())
555
+ except OSError:
556
+ resolved = str(path)
557
+ if resolved in seen or not path.exists():
558
+ continue
559
+ seen.add(resolved)
560
+ try:
561
+ size = path.stat().st_size
562
+ except OSError:
563
+ size = 0
564
+ stores.append({"label": label, "path": str(path), "size": size})
565
+ return stores
566
+
567
+
519
568
  def cmd_status(args: argparse.Namespace) -> int:
520
569
  """Show local + server-side state without modifying anything.
521
570
 
@@ -552,6 +601,27 @@ def cmd_status(args: argparse.Namespace) -> int:
552
601
  else:
553
602
  print(a.kv("DB path", f"{db_path} (not created)"))
554
603
 
604
+ # All resolvable stores on this machine (split-brain visibility floor,
605
+ # VRTX beta report 2026-06-11). Read-only: lists what exists, moves
606
+ # nothing. A divergence warning fires when more than one store holds data,
607
+ # because that is exactly when an agent "loses" memory by switching the
608
+ # entry point it reads from.
609
+ stores = _discover_stores(db_path)
610
+ if len(stores) > 1:
611
+ print()
612
+ print(a.eyebrow("memory stores"))
613
+ for s in stores:
614
+ mb = s["size"] / (1024 * 1024)
615
+ print(a.kv(s["label"], f"{s['path']} ({s['size']:,} bytes · {mb:.2f} MB)"))
616
+ with_data = [s for s in stores if s["size"] > 0]
617
+ if len(with_data) > 1:
618
+ print()
619
+ print(a.warn_line("Multiple memory stores hold data on this machine."))
620
+ print(a.dim(" Memory is NOT shared across these paths. An agent reads only the store"))
621
+ print(a.dim(" for its entry point (SDK/CLI vs Hermes vs profile vs MCP), so memory can"))
622
+ print(a.dim(" look 'missing' when you switch. Point every entry point at one path via"))
623
+ print(a.dim(" --db / SIBYL_MEMORY_DB, or back up and consolidate before relying on recall."))
624
+
555
625
  tier_cache = Path(args.tier_cache).expanduser()
556
626
  if tier_cache.exists():
557
627
  cache = json.loads(tier_cache.read_text(encoding="utf-8"))
@@ -1144,6 +1214,22 @@ def build_parser() -> argparse.ArgumentParser:
1144
1214
  p_h = sub.add_parser("health", help="Run the provider self-check")
1145
1215
  p_h.set_defaults(func=cmd_health)
1146
1216
 
1217
+ p_mem = sub.add_parser("memory", help="Read-only inspection of your memory store (list / search / recall)")
1218
+ mem_sub = p_mem.add_subparsers(dest="mem_cmd")
1219
+ p_ml = mem_sub.add_parser("list", help="List entities (optionally filtered by category)")
1220
+ p_ml.add_argument("category", nargs="?", default=None, help="Optional category to filter by")
1221
+ p_ml.add_argument("--limit", type=int, default=50, help="Max rows (default 50)")
1222
+ p_ml.set_defaults(func=cmd_memory)
1223
+ p_ms = mem_sub.add_parser("search", help="Full-text search across entities + state + reference + journal")
1224
+ p_ms.add_argument("query", help="Search query (matches stored text, not meaning)")
1225
+ p_ms.add_argument("--limit", type=int, default=20, help="Max hits (default 20)")
1226
+ p_ms.set_defaults(func=cmd_memory)
1227
+ p_mr = mem_sub.add_parser("recall", help="Recall one entity by category + name")
1228
+ p_mr.add_argument("category", help="Entity category")
1229
+ p_mr.add_argument("name", help="Entity name")
1230
+ p_mr.set_defaults(func=cmd_memory)
1231
+ p_mem.set_defaults(func=cmd_memory)
1232
+
1147
1233
  p_update = sub.add_parser(
1148
1234
  "update",
1149
1235
  help="Check for newer sibyl-memory-* releases on PyPI (use --apply to upgrade)",
@@ -1214,6 +1300,61 @@ def build_parser() -> argparse.ArgumentParser:
1214
1300
  return p
1215
1301
 
1216
1302
 
1303
+ def cmd_memory(args: argparse.Namespace) -> int:
1304
+ """Read-only inspection of the local memory store (PKG-4, VRTX/deadguy beta).
1305
+
1306
+ sibyl memory list [category] list entities
1307
+ sibyl memory search <query> full-text search across tiers
1308
+ sibyl memory recall <cat> <nm> recall one entity by category + name
1309
+
1310
+ Opens the resolved DB read-only via the SDK; never writes. Respects --db so
1311
+ you can inspect any split-brain store that `sibyl status` surfaces.
1312
+ """
1313
+ from sibyl_memory_client import MemoryClient
1314
+
1315
+ db_path = Path(args.db).expanduser()
1316
+ print()
1317
+ if not db_path.exists():
1318
+ print(a.warn_line(f"No memory store at {db_path}."))
1319
+ print(a.dim(" Run `sibyl status` to see every store on this machine."))
1320
+ return 1
1321
+ client = MemoryClient.local(path=db_path)
1322
+ op = getattr(args, "mem_cmd", None)
1323
+ if op == "list":
1324
+ rows = client.list_entities(category=args.category, limit=args.limit)
1325
+ if not rows:
1326
+ print(a.dim("(no entities)"))
1327
+ return 0
1328
+ print(a.eyebrow(f"entities ({len(rows)})"))
1329
+ for r in rows:
1330
+ print(a.kv(f"{r['category']}/{r['name']}", r.get("status") or "-"))
1331
+ return 0
1332
+ if op == "search":
1333
+ hits = client.search(args.query, limit=args.limit)
1334
+ if not hits:
1335
+ print(a.dim(f"(no matches for {args.query!r})"))
1336
+ return 0
1337
+ print(a.eyebrow(f"matches ({len(hits)})"))
1338
+ for h in hits:
1339
+ snip = (h.get("snippet") or "").replace("\n", " ")[:100]
1340
+ print(a.kv(f"[{h.get('tier') or '-'}] {h.get('key') or '-'}", snip))
1341
+ return 0
1342
+ if op == "recall":
1343
+ try:
1344
+ ent = client.get_entity(args.category, args.name)
1345
+ except Exception as e:
1346
+ print(a.warn_line(str(e)))
1347
+ return 1
1348
+ print(a.eyebrow(f"{ent['category']}/{ent['name']}"))
1349
+ print(a.kv("status", ent.get("status") or "-"))
1350
+ print(a.kv("updated", ent.get("updated_at") or "-"))
1351
+ body = ent.get("body")
1352
+ print(body if isinstance(body, str) else json.dumps(body, indent=2, ensure_ascii=False))
1353
+ return 0
1354
+ print(a.warn_line("Usage: sibyl memory {list|search|recall} ..."))
1355
+ return 1
1356
+
1357
+
1217
1358
  def main(argv: list[str] | None = None) -> int:
1218
1359
  parser = build_parser()
1219
1360
  args = parser.parse_args(argv)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT
@@ -14,9 +14,11 @@ src/sibyl_memory_cli.egg-info/requires.txt
14
14
  src/sibyl_memory_cli.egg-info/top_level.txt
15
15
  tests/test_hermes_install_args.py
16
16
  tests/test_init_perms_guard.py
17
+ tests/test_memory_cmd_2026_06_16.py
17
18
  tests/test_migrate.py
18
19
  tests/test_migrate_adversarial.py
19
20
  tests/test_migrate_force_2026_06_05.py
20
21
  tests/test_orchestrator.py
21
22
  tests/test_setup.py
23
+ tests/test_status_stores_2026_06_11.py
22
24
  tests/test_wiring_fix.py
@@ -0,0 +1,55 @@
1
+ """PKG-4 (VRTX/deadguy beta): read-only `sibyl memory list/search/recall` CLI.
2
+ Lets a tester inspect what's actually stored without writing through an agent."""
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from sibyl_memory_client import MemoryClient
8
+ from sibyl_memory_cli import cli
9
+
10
+
11
+ def _store(tmp_path: Path) -> Path:
12
+ d = tmp_path / "memory.db"
13
+ c = MemoryClient.local(path=d)
14
+ c.set_entity("partner", "Blocktronics", {"stage": "active", "note": "token forensics suite"})
15
+ c.set_entity("partner", "Reppo", {"stage": "negotiation"})
16
+ return d
17
+
18
+
19
+ def test_memory_list(tmp_path, capsys):
20
+ d = _store(tmp_path)
21
+ rc = cli.main(["--db", str(d), "memory", "list"])
22
+ out = capsys.readouterr().out
23
+ assert rc == 0
24
+ assert "Blocktronics" in out and "Reppo" in out
25
+
26
+
27
+ def test_memory_list_category_filter(tmp_path, capsys):
28
+ d = _store(tmp_path)
29
+ rc = cli.main(["--db", str(d), "memory", "list", "partner", "--limit", "1"])
30
+ assert rc == 0
31
+
32
+
33
+ def test_memory_search(tmp_path, capsys):
34
+ d = _store(tmp_path)
35
+ rc = cli.main(["--db", str(d), "memory", "search", "forensics"])
36
+ out = capsys.readouterr().out
37
+ assert rc == 0 and "Blocktronics" in out
38
+
39
+
40
+ def test_memory_recall(tmp_path, capsys):
41
+ d = _store(tmp_path)
42
+ rc = cli.main(["--db", str(d), "memory", "recall", "partner", "Blocktronics"])
43
+ out = capsys.readouterr().out
44
+ assert rc == 0 and "forensics" in out
45
+
46
+
47
+ def test_memory_recall_missing_returns_1(tmp_path):
48
+ d = _store(tmp_path)
49
+ rc = cli.main(["--db", str(d), "memory", "recall", "partner", "Nope"])
50
+ assert rc == 1
51
+
52
+
53
+ def test_memory_no_store_returns_1(tmp_path):
54
+ rc = cli.main(["--db", str(tmp_path / "absent.db"), "memory", "list"])
55
+ assert rc == 1
@@ -0,0 +1,70 @@
1
+ """PKG-2/3 regression: `sibyl status` discovers + lists every memory store and
2
+ warns on split-brain divergence (VRTX beta report 2026-06-11).
3
+
4
+ Read-only: discovery never creates or moves a DB.
5
+ """
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from sibyl_memory_cli import cli
13
+
14
+
15
+ @pytest.fixture
16
+ def sandbox(monkeypatch):
17
+ d = Path(tempfile.mkdtemp())
18
+ primary = d / ".sibyl-memory" / "memory.db"
19
+ primary.parent.mkdir(parents=True)
20
+ hermes = d / ".hermes"
21
+ (hermes / "sibyl").mkdir(parents=True)
22
+ monkeypatch.setenv("HERMES_HOME", str(hermes))
23
+ monkeypatch.delenv("SIBYL_MEMORY_DB", raising=False)
24
+ return d, primary, hermes
25
+
26
+
27
+ def test_discovers_default_hermes_and_profiles(sandbox):
28
+ d, primary, hermes = sandbox
29
+ primary.write_bytes(b"x" * 100)
30
+ (hermes / "sibyl" / "memory.db").write_bytes(b"y" * 200)
31
+ prof = hermes / "sibyl" / "profiles" / "alpha"
32
+ prof.mkdir(parents=True)
33
+ (prof / "memory.db").write_bytes(b"z" * 50)
34
+
35
+ stores = cli._discover_stores(primary)
36
+ labels = {s["label"] for s in stores}
37
+ assert "default (SDK/CLI/MCP)" in labels
38
+ assert "hermes adapter" in labels
39
+ assert any(l.startswith("hermes profile") for l in labels)
40
+ assert {s["size"] for s in stores} == {100, 200, 50}
41
+
42
+
43
+ def test_skips_nonexistent_and_dedups(sandbox):
44
+ d, primary, hermes = sandbox
45
+ primary.write_bytes(b"x" * 10)
46
+ # hermes adapter db does not exist → excluded
47
+ stores = cli._discover_stores(primary)
48
+ assert [s["label"] for s in stores] == ["default (SDK/CLI/MCP)"]
49
+
50
+
51
+ def test_mcp_override_included(sandbox, monkeypatch):
52
+ d, primary, hermes = sandbox
53
+ primary.write_bytes(b"x" * 10)
54
+ shadow = d / "shadow.db"
55
+ shadow.write_bytes(b"s" * 5)
56
+ monkeypatch.setenv("SIBYL_MEMORY_DB", str(shadow))
57
+ stores = cli._discover_stores(primary)
58
+ assert any(s["label"] == "MCP SIBYL_MEMORY_DB" for s in stores)
59
+
60
+
61
+ def test_status_warns_on_divergence(sandbox, capsys):
62
+ d, primary, hermes = sandbox
63
+ primary.write_bytes(b"x" * 100)
64
+ (hermes / "sibyl" / "memory.db").write_bytes(b"y" * 200)
65
+
66
+ # Build the minimal args cmd_status reads. No credentials → early-return
67
+ # path, so drive discovery directly to assert the warning copy instead.
68
+ stores = cli._discover_stores(primary)
69
+ with_data = [s for s in stores if s["size"] > 0]
70
+ assert len(with_data) > 1 # divergence condition that triggers the warning