sibyl-memory-cli 0.3.11__tar.gz → 0.3.13__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 (24) hide show
  1. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/PKG-INFO +1 -1
  2. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/pyproject.toml +1 -1
  3. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/cli.py +15 -2
  4. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/migrate.py +2 -2
  5. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/setup.py +32 -6
  6. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/PKG-INFO +1 -1
  7. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/SOURCES.txt +1 -0
  8. sibyl_memory_cli-0.3.13/tests/test_migrate_force_2026_06_05.py +67 -0
  9. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/README.md +0 -0
  10. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/setup.cfg +0 -0
  11. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/__init__.py +0 -0
  12. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/_aesthetic.py +0 -0
  13. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/_banner.py +0 -0
  14. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
  15. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
  16. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
  17. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
  18. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_hermes_install_args.py +0 -0
  19. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_init_perms_guard.py +0 -0
  20. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_migrate.py +0 -0
  21. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_migrate_adversarial.py +0 -0
  22. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_orchestrator.py +0 -0
  23. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_setup.py +0 -0
  24. {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/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.11
3
+ Version: 0.3.13
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.11"
7
+ version = "0.3.13"
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" }
@@ -555,7 +555,15 @@ def cmd_status(args: argparse.Namespace) -> int:
555
555
  tier_cache = Path(args.tier_cache).expanduser()
556
556
  if tier_cache.exists():
557
557
  cache = json.loads(tier_cache.read_text(encoding="utf-8"))
558
- print(a.kv("Tier cache", f"{cache.get('tier','?')} (checked {cache.get('checked_at','?')[:19]})"))
558
+ # checked_at is written by _capcheck.py as epoch seconds (float), but
559
+ # older caches / future formats may carry an ISO string. Render both;
560
+ # never index a float (TypeError on every `sibyl status` run with a
561
+ # populated tier cache; Discord report 2026-06-10).
562
+ checked = cache.get("checked_at")
563
+ if isinstance(checked, (int, float)) and not isinstance(checked, bool):
564
+ checked = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(checked))
565
+ checked = str(checked)[:19] if checked else "?"
566
+ print(a.kv("Tier cache", f"{cache.get('tier','?')} (checked {checked})"))
559
567
  else:
560
568
  print(a.kv("Tier cache", "—"))
561
569
 
@@ -1056,7 +1064,7 @@ def cmd_migrate(args: argparse.Namespace) -> int:
1056
1064
  io = _migrate_io()
1057
1065
  report = M.run_guided_setup(
1058
1066
  home=home, cwd=cwd, db_path=db_path, backup_parent=backup_parent,
1059
- io=io, debloat=not args.no_debloat,
1067
+ io=io, debloat=not args.no_debloat, force=getattr(args, "force", False),
1060
1068
  )
1061
1069
 
1062
1070
  ph = report.get("phases", {})
@@ -1196,6 +1204,11 @@ def build_parser() -> argparse.ArgumentParser:
1196
1204
  "--yes", "-y", action="store_true",
1197
1205
  help="Skip the initial confirm (the trim step still always asks separately)",
1198
1206
  )
1207
+ p_migrate.add_argument(
1208
+ "--force", action="store_true",
1209
+ help="Overwrite an existing non-sibyl memory provider when wiring a harness "
1210
+ "(without this, migrate stops at that harness and tells you to re-run with --force)",
1211
+ )
1199
1212
  p_migrate.set_defaults(func=cmd_migrate)
1200
1213
 
1201
1214
  return p
@@ -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
- pass
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
- try:
497
- loaded = json.loads(self.settings_path.read_text(encoding="utf-8"))
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
- except Exception:
501
- cfg = {}
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.11
3
+ Version: 0.3.13
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
@@ -16,6 +16,7 @@ tests/test_hermes_install_args.py
16
16
  tests/test_init_perms_guard.py
17
17
  tests/test_migrate.py
18
18
  tests/test_migrate_adversarial.py
19
+ tests/test_migrate_force_2026_06_05.py
19
20
  tests/test_orchestrator.py
20
21
  tests/test_setup.py
21
22
  tests/test_wiring_fix.py
@@ -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