sibyl-memory-cli 0.3.11__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.
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/PKG-INFO +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/pyproject.toml +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/cli.py +156 -2
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/migrate.py +2 -2
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/setup.py +32 -6
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/PKG-INFO +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/SOURCES.txt +3 -0
- sibyl_memory_cli-0.3.15/tests/test_memory_cmd_2026_06_16.py +55 -0
- sibyl_memory_cli-0.3.15/tests/test_migrate_force_2026_06_05.py +67 -0
- sibyl_memory_cli-0.3.15/tests/test_status_stores_2026_06_11.py +70 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/README.md +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/setup.cfg +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/__init__.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/_aesthetic.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_hermes_install_args.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_init_perms_guard.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_migrate.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_migrate_adversarial.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_orchestrator.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/tests/test_setup.py +0 -0
- {sibyl_memory_cli-0.3.11 → 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.
|
|
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.
|
|
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,10 +601,39 @@ 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"))
|
|
558
|
-
|
|
628
|
+
# checked_at is written by _capcheck.py as epoch seconds (float), but
|
|
629
|
+
# older caches / future formats may carry an ISO string. Render both;
|
|
630
|
+
# never index a float (TypeError on every `sibyl status` run with a
|
|
631
|
+
# populated tier cache; Discord report 2026-06-10).
|
|
632
|
+
checked = cache.get("checked_at")
|
|
633
|
+
if isinstance(checked, (int, float)) and not isinstance(checked, bool):
|
|
634
|
+
checked = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(checked))
|
|
635
|
+
checked = str(checked)[:19] if checked else "?"
|
|
636
|
+
print(a.kv("Tier cache", f"{cache.get('tier','?')} (checked {checked})"))
|
|
559
637
|
else:
|
|
560
638
|
print(a.kv("Tier cache", "—"))
|
|
561
639
|
|
|
@@ -1056,7 +1134,7 @@ def cmd_migrate(args: argparse.Namespace) -> int:
|
|
|
1056
1134
|
io = _migrate_io()
|
|
1057
1135
|
report = M.run_guided_setup(
|
|
1058
1136
|
home=home, cwd=cwd, db_path=db_path, backup_parent=backup_parent,
|
|
1059
|
-
io=io, debloat=not args.no_debloat,
|
|
1137
|
+
io=io, debloat=not args.no_debloat, force=getattr(args, "force", False),
|
|
1060
1138
|
)
|
|
1061
1139
|
|
|
1062
1140
|
ph = report.get("phases", {})
|
|
@@ -1136,6 +1214,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1136
1214
|
p_h = sub.add_parser("health", help="Run the provider self-check")
|
|
1137
1215
|
p_h.set_defaults(func=cmd_health)
|
|
1138
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
|
+
|
|
1139
1233
|
p_update = sub.add_parser(
|
|
1140
1234
|
"update",
|
|
1141
1235
|
help="Check for newer sibyl-memory-* releases on PyPI (use --apply to upgrade)",
|
|
@@ -1196,11 +1290,71 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1196
1290
|
"--yes", "-y", action="store_true",
|
|
1197
1291
|
help="Skip the initial confirm (the trim step still always asks separately)",
|
|
1198
1292
|
)
|
|
1293
|
+
p_migrate.add_argument(
|
|
1294
|
+
"--force", action="store_true",
|
|
1295
|
+
help="Overwrite an existing non-sibyl memory provider when wiring a harness "
|
|
1296
|
+
"(without this, migrate stops at that harness and tells you to re-run with --force)",
|
|
1297
|
+
)
|
|
1199
1298
|
p_migrate.set_defaults(func=cmd_migrate)
|
|
1200
1299
|
|
|
1201
1300
|
return p
|
|
1202
1301
|
|
|
1203
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
|
+
|
|
1204
1358
|
def main(argv: list[str] | None = None) -> int:
|
|
1205
1359
|
parser = build_parser()
|
|
1206
1360
|
args = parser.parse_args(argv)
|
|
@@ -330,7 +330,7 @@ class GuidedIO:
|
|
|
330
330
|
def run_guided_setup(*, home=None, cwd=None, db_path=None, backup_parent=None,
|
|
331
331
|
io: Optional[GuidedIO] = None, wirers: Optional[dict] = None,
|
|
332
332
|
extract_fn: Optional[Callable[[Path, Path], None]] = None,
|
|
333
|
-
debloat: bool = True, now=None) -> dict:
|
|
333
|
+
debloat: bool = True, force: bool = False, now=None) -> dict:
|
|
334
334
|
"""The assembled guided flow: backup -> auto-wire each harness (instructions on
|
|
335
335
|
failure) -> extraction handoff -> verify -> confirmed debloat. Returns a structured
|
|
336
336
|
report. `extract_fn(backup_dir, db_path)` performs/simulates extraction; default
|
|
@@ -367,7 +367,7 @@ def run_guided_setup(*, home=None, cwd=None, db_path=None, backup_parent=None,
|
|
|
367
367
|
if w.current_state().get("wired_with_sibyl"):
|
|
368
368
|
wire_report[name] = "already"
|
|
369
369
|
continue
|
|
370
|
-
outcome = w.wire()
|
|
370
|
+
outcome = w.wire(force=force)
|
|
371
371
|
wire_report[name] = outcome.status
|
|
372
372
|
if outcome.status not in ("wired", "already"):
|
|
373
373
|
io.say(f"{name}: auto-wire incomplete ({outcome.message}). Do this manually:")
|
|
@@ -222,6 +222,15 @@ class HermesWirer:
|
|
|
222
222
|
loaded = yaml.safe_load(raw)
|
|
223
223
|
if isinstance(loaded, dict):
|
|
224
224
|
cfg = loaded
|
|
225
|
+
elif loaded is not None:
|
|
226
|
+
# Fail fast: a non-mapping top level means the config is not
|
|
227
|
+
# something we can merge into. Silently reinitializing would
|
|
228
|
+
# destroy the user's existing Hermes settings.
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"{self.config_path} top level is not a YAML mapping "
|
|
231
|
+
f"(got {type(loaded).__name__}). Fix the file or move it "
|
|
232
|
+
"aside and re-run; it was not modified."
|
|
233
|
+
)
|
|
225
234
|
if not isinstance(cfg.get("memory"), dict):
|
|
226
235
|
cfg["memory"] = {}
|
|
227
236
|
cfg["memory"]["provider"] = "sibyl"
|
|
@@ -338,6 +347,7 @@ class ClaudeCodeWirer:
|
|
|
338
347
|
settings_exists = self.settings_path.exists()
|
|
339
348
|
mcp_servers: dict = {}
|
|
340
349
|
sibyl_block: Optional[dict] = None
|
|
350
|
+
settings_parse_error: Optional[str] = None
|
|
341
351
|
if settings_exists:
|
|
342
352
|
try:
|
|
343
353
|
cfg = json.loads(self.settings_path.read_text(encoding="utf-8"))
|
|
@@ -346,8 +356,8 @@ class ClaudeCodeWirer:
|
|
|
346
356
|
if isinstance(raw_servers, dict):
|
|
347
357
|
mcp_servers = raw_servers
|
|
348
358
|
sibyl_block = mcp_servers.get("sibyl-memory")
|
|
349
|
-
except Exception:
|
|
350
|
-
|
|
359
|
+
except Exception as e:
|
|
360
|
+
settings_parse_error = f"{type(e).__name__}: {e}"
|
|
351
361
|
mcp_binary = self._mcp_binary_found()
|
|
352
362
|
cli_registered = self._registered_via_cli() # None when no `claude` CLI
|
|
353
363
|
# Source of truth: when the claude CLI exists, trust `claude mcp get` (where
|
|
@@ -359,6 +369,7 @@ class ClaudeCodeWirer:
|
|
|
359
369
|
return {
|
|
360
370
|
"settings_path": str(self.settings_path),
|
|
361
371
|
"settings_exists": settings_exists,
|
|
372
|
+
"settings_parse_error": settings_parse_error,
|
|
362
373
|
"mcp_servers_count": len(mcp_servers),
|
|
363
374
|
"sibyl_mcp": sibyl_block,
|
|
364
375
|
"mcp_binary_found": mcp_binary,
|
|
@@ -493,12 +504,27 @@ class ClaudeCodeWirer:
|
|
|
493
504
|
def _write_settings_with_sibyl(self) -> None:
|
|
494
505
|
cfg: dict = {}
|
|
495
506
|
if self.settings_path.exists():
|
|
496
|
-
|
|
497
|
-
|
|
507
|
+
raw = self.settings_path.read_text(encoding="utf-8")
|
|
508
|
+
if raw.strip():
|
|
509
|
+
try:
|
|
510
|
+
loaded = json.loads(raw)
|
|
511
|
+
except ValueError as e:
|
|
512
|
+
# Fail fast: never atomically replace a corrupt settings.json
|
|
513
|
+
# with a sibyl-only file (that destroys the user's other
|
|
514
|
+
# mcpServers, permissions, hooks, env).
|
|
515
|
+
raise ValueError(
|
|
516
|
+
f"{self.settings_path} is not valid JSON ({e}). "
|
|
517
|
+
"Fix the file or move it aside and re-run setup; "
|
|
518
|
+
"your settings were not modified."
|
|
519
|
+
) from e
|
|
498
520
|
if isinstance(loaded, dict):
|
|
499
521
|
cfg = loaded
|
|
500
|
-
|
|
501
|
-
|
|
522
|
+
else:
|
|
523
|
+
raise ValueError(
|
|
524
|
+
f"{self.settings_path} top level is not a JSON object "
|
|
525
|
+
f"(got {type(loaded).__name__}). Fix the file or move it "
|
|
526
|
+
"aside and re-run setup; your settings were not modified."
|
|
527
|
+
)
|
|
502
528
|
if not isinstance(cfg.get("mcpServers"), dict):
|
|
503
529
|
cfg["mcpServers"] = {}
|
|
504
530
|
cfg["mcpServers"]["sibyl-memory"] = self.SIBYL_MCP_BLOCK
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.3.
|
|
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
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -14,8 +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
|
|
20
|
+
tests/test_migrate_force_2026_06_05.py
|
|
19
21
|
tests/test_orchestrator.py
|
|
20
22
|
tests/test_setup.py
|
|
23
|
+
tests/test_status_stores_2026_06_11.py
|
|
21
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,67 @@
|
|
|
1
|
+
"""Regression (bugflow 2026-06-05): `sibyl migrate --force` must reach the wirers.
|
|
2
|
+
|
|
3
|
+
Onboarding dead-end: when a detected harness already had a non-sibyl memory
|
|
4
|
+
provider, the wirer refused with "Use --force to overwrite." but
|
|
5
|
+
`run_guided_setup` called `wire()` with no `force`, and `sibyl migrate` had no
|
|
6
|
+
`--force` flag to pass. The flag now threads cli -> run_guided_setup(force=) ->
|
|
7
|
+
wire(force=force).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from sibyl_memory_cli import migrate as M
|
|
12
|
+
from sibyl_memory_cli.setup import WireOutcome
|
|
13
|
+
from sibyl_memory_client import MemoryClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _RecordingWirer:
|
|
17
|
+
"""A fake harness wirer that records the `force` kwarg it was called with."""
|
|
18
|
+
|
|
19
|
+
name = "rec"
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.seen_force = None
|
|
23
|
+
|
|
24
|
+
def is_present(self):
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
def current_state(self):
|
|
28
|
+
return {"wired_with_sibyl": False}
|
|
29
|
+
|
|
30
|
+
def wire(self, *, force: bool = False, dry_run: bool = False, prompt_fn=None):
|
|
31
|
+
self.seen_force = force
|
|
32
|
+
return WireOutcome(self.name, "wired", "ok")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _home_with_memory(tmp_path):
|
|
36
|
+
h = tmp_path / "home"
|
|
37
|
+
(h / "proj").mkdir(parents=True)
|
|
38
|
+
(h / "proj" / "CLAUDE.md").write_text("# memory\n- a fact worth keeping\n")
|
|
39
|
+
db = h / ".sibyl-memory" / "memory.db"
|
|
40
|
+
db.parent.mkdir(parents=True)
|
|
41
|
+
return h, db
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _fake_extract(_backup_dir, db_path):
|
|
45
|
+
MemoryClient.local(str(db_path), tenant_id="qa").set_entity("f", "a", {"v": 1})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_force_true_threads_to_wirer(tmp_path):
|
|
49
|
+
h, db = _home_with_memory(tmp_path)
|
|
50
|
+
rec = _RecordingWirer()
|
|
51
|
+
M.run_guided_setup(
|
|
52
|
+
home=h, cwd=h / "proj", db_path=db, backup_parent=tmp_path / "bk",
|
|
53
|
+
io=M.GuidedIO(scripted=["n"]), wirers={"rec": rec},
|
|
54
|
+
extract_fn=_fake_extract, force=True,
|
|
55
|
+
)
|
|
56
|
+
assert rec.seen_force is True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_force_defaults_false(tmp_path):
|
|
60
|
+
h, db = _home_with_memory(tmp_path)
|
|
61
|
+
rec = _RecordingWirer()
|
|
62
|
+
M.run_guided_setup(
|
|
63
|
+
home=h, cwd=h / "proj", db_path=db, backup_parent=tmp_path / "bk",
|
|
64
|
+
io=M.GuidedIO(scripted=["n"]), wirers={"rec": rec},
|
|
65
|
+
extract_fn=_fake_extract,
|
|
66
|
+
)
|
|
67
|
+
assert rec.seen_force is False
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.15}/src/sibyl_memory_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|