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.
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/PKG-INFO +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/pyproject.toml +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/cli.py +15 -2
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/migrate.py +2 -2
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/setup.py +32 -6
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/PKG-INFO +1 -1
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/SOURCES.txt +1 -0
- sibyl_memory_cli-0.3.13/tests/test_migrate_force_2026_06_05.py +67 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/README.md +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/setup.cfg +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/__init__.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/_aesthetic.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_hermes_install_args.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_init_perms_guard.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_migrate.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_migrate_adversarial.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_orchestrator.py +0 -0
- {sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/tests/test_setup.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.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
|
|
@@ -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
|
|
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.13}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/src/sibyl_memory_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.11 → sibyl_memory_cli-0.3.13}/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
|