sibyl-memory-cli 0.3.5__tar.gz → 0.3.7__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.5 → sibyl_memory_cli-0.3.7}/PKG-INFO +15 -19
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/README.md +14 -18
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/pyproject.toml +1 -1
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli/cli.py +170 -2
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli/setup.py +174 -7
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/PKG-INFO +15 -19
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/tests/test_setup.py +1 -1
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/setup.cfg +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli/__init__.py +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli/_aesthetic.py +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/SOURCES.txt +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
- {sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/top_level.txt +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.7
|
|
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
|
|
@@ -48,39 +48,37 @@ sibyl health Run the SibylMemoryProvider self-check (schema versi
|
|
|
48
48
|
```bash
|
|
49
49
|
$ sibyl init
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Sibyl Memory Plugin · activation
|
|
52
52
|
|
|
53
|
-
Session
|
|
54
|
-
|
|
55
|
-
Opening https://auth.sibyllabs.org/a1b2c3d4-…
|
|
53
|
+
Session: a1b2c3d4…e5f6
|
|
54
|
+
Opening: https://sibyllabs.org/plugin/activate?session=a1b2c3d4-…
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
this terminal will pick up automatically when you bind.
|
|
56
|
+
Sign in with your wallet in the browser. This terminal will pick up automatically.
|
|
59
57
|
|
|
60
|
-
⠹
|
|
58
|
+
⠹ waiting for browser activation … 9:42 left
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
The browser opens
|
|
61
|
+
The browser opens. Sign a SIWE message with your wallet. The terminal picks up the moment the binding lands. Credentials are written to `~/.sibyl-memory/credentials.json` at mode 0600.
|
|
64
62
|
|
|
65
63
|
## Upgrade
|
|
66
64
|
|
|
67
65
|
```bash
|
|
68
66
|
$ sibyl upgrade
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Sibyl Memory Plugin · upgrade
|
|
71
69
|
|
|
72
70
|
Account a1b2c3d4…e5f6
|
|
73
71
|
Current tier FREE
|
|
74
72
|
Opening https://sibyllabs.org/plugin/upgrade?session=…
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
74
|
+
Two paths in your browser:
|
|
75
|
+
1. Stake $SIBYL on Base (free unlimited if you qualify)
|
|
76
|
+
2. Subscribe in USDC (monthly / quarterly / annual)
|
|
79
77
|
```
|
|
80
78
|
|
|
81
79
|
In the browser:
|
|
82
|
-
- **Stake
|
|
83
|
-
- **Subscribe
|
|
80
|
+
- **Stake**: connect your wallet (browser or Coinbase Smart Wallet), sign to bind, and the page checks your `$SIBYL` balance on Base. If you hold the threshold (default 100,000 $SIBYL liquid+staked, configurable), the local cap lifts.
|
|
81
|
+
- **Subscribe**: pick monthly ($29) / quarterly ($79) / annual ($290) USDC, sign the transfer, the server records the subscription. Tier flips immediately.
|
|
84
82
|
|
|
85
83
|
On either path, the CLI sees the tier change, rewrites `credentials.json`, and clears `tier_cache.json` so your next write picks up the new entitlement without delay.
|
|
86
84
|
|
|
@@ -115,16 +113,14 @@ For internal testing only:
|
|
|
115
113
|
|
|
116
114
|
```bash
|
|
117
115
|
SIBYL_API_BASE=https://staging.api.sibyllabs.org sibyl init
|
|
118
|
-
SIBYL_ACTIVATE_BASE=https://staging.
|
|
116
|
+
SIBYL_ACTIVATE_BASE=https://staging.sibyllabs.org/plugin/activate sibyl init
|
|
119
117
|
SIBYL_UPGRADE_BASE=https://staging.sibyllabs.org/plugin/upgrade sibyl upgrade
|
|
120
118
|
```
|
|
121
119
|
|
|
122
|
-
`SIBYL_ACTIVATE_BASE` accepts both subdomain form (`https://auth.example.org` → path-based session URL: `https://auth.example.org/<uuid>`) and legacy path form (`https://example.org/plugin/activate` → query-based: `?session=<uuid>`). The CLI auto-detects from the hostname.
|
|
123
|
-
|
|
124
120
|
## Security
|
|
125
121
|
|
|
126
122
|
- `credentials.json` is written atomically at mode 0600.
|
|
127
|
-
- `session_token` is never printed in full
|
|
123
|
+
- `session_token` is never printed in full: only a short slice.
|
|
128
124
|
- No memory content ever transits these endpoints. The CLI never reads `memory.db` content; it only checks file size.
|
|
129
125
|
- Wallet operations happen in the browser. The CLI sees only the resulting tier change.
|
|
130
126
|
|
|
@@ -22,39 +22,37 @@ sibyl health Run the SibylMemoryProvider self-check (schema versi
|
|
|
22
22
|
```bash
|
|
23
23
|
$ sibyl init
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Sibyl Memory Plugin · activation
|
|
26
26
|
|
|
27
|
-
Session
|
|
28
|
-
|
|
29
|
-
Opening https://auth.sibyllabs.org/a1b2c3d4-…
|
|
27
|
+
Session: a1b2c3d4…e5f6
|
|
28
|
+
Opening: https://sibyllabs.org/plugin/activate?session=a1b2c3d4-…
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
this terminal will pick up automatically when you bind.
|
|
30
|
+
Sign in with your wallet in the browser. This terminal will pick up automatically.
|
|
33
31
|
|
|
34
|
-
⠹
|
|
32
|
+
⠹ waiting for browser activation … 9:42 left
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
The browser opens
|
|
35
|
+
The browser opens. Sign a SIWE message with your wallet. The terminal picks up the moment the binding lands. Credentials are written to `~/.sibyl-memory/credentials.json` at mode 0600.
|
|
38
36
|
|
|
39
37
|
## Upgrade
|
|
40
38
|
|
|
41
39
|
```bash
|
|
42
40
|
$ sibyl upgrade
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
Sibyl Memory Plugin · upgrade
|
|
45
43
|
|
|
46
44
|
Account a1b2c3d4…e5f6
|
|
47
45
|
Current tier FREE
|
|
48
46
|
Opening https://sibyllabs.org/plugin/upgrade?session=…
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
1.
|
|
52
|
-
2.
|
|
48
|
+
Two paths in your browser:
|
|
49
|
+
1. Stake $SIBYL on Base (free unlimited if you qualify)
|
|
50
|
+
2. Subscribe in USDC (monthly / quarterly / annual)
|
|
53
51
|
```
|
|
54
52
|
|
|
55
53
|
In the browser:
|
|
56
|
-
- **Stake
|
|
57
|
-
- **Subscribe
|
|
54
|
+
- **Stake**: connect your wallet (browser or Coinbase Smart Wallet), sign to bind, and the page checks your `$SIBYL` balance on Base. If you hold the threshold (default 100,000 $SIBYL liquid+staked, configurable), the local cap lifts.
|
|
55
|
+
- **Subscribe**: pick monthly ($29) / quarterly ($79) / annual ($290) USDC, sign the transfer, the server records the subscription. Tier flips immediately.
|
|
58
56
|
|
|
59
57
|
On either path, the CLI sees the tier change, rewrites `credentials.json`, and clears `tier_cache.json` so your next write picks up the new entitlement without delay.
|
|
60
58
|
|
|
@@ -89,16 +87,14 @@ For internal testing only:
|
|
|
89
87
|
|
|
90
88
|
```bash
|
|
91
89
|
SIBYL_API_BASE=https://staging.api.sibyllabs.org sibyl init
|
|
92
|
-
SIBYL_ACTIVATE_BASE=https://staging.
|
|
90
|
+
SIBYL_ACTIVATE_BASE=https://staging.sibyllabs.org/plugin/activate sibyl init
|
|
93
91
|
SIBYL_UPGRADE_BASE=https://staging.sibyllabs.org/plugin/upgrade sibyl upgrade
|
|
94
92
|
```
|
|
95
93
|
|
|
96
|
-
`SIBYL_ACTIVATE_BASE` accepts both subdomain form (`https://auth.example.org` → path-based session URL: `https://auth.example.org/<uuid>`) and legacy path form (`https://example.org/plugin/activate` → query-based: `?session=<uuid>`). The CLI auto-detects from the hostname.
|
|
97
|
-
|
|
98
94
|
## Security
|
|
99
95
|
|
|
100
96
|
- `credentials.json` is written atomically at mode 0600.
|
|
101
|
-
- `session_token` is never printed in full
|
|
97
|
+
- `session_token` is never printed in full: only a short slice.
|
|
102
98
|
- No memory content ever transits these endpoints. The CLI never reads `memory.db` content; it only checks file size.
|
|
103
99
|
- Wallet operations happen in the browser. The CLI sees only the resulting tier change.
|
|
104
100
|
|
|
@@ -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.7"
|
|
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" }
|
|
@@ -825,6 +825,162 @@ def cmd_health(args: argparse.Namespace) -> int:
|
|
|
825
825
|
return 0 if ok_state else 1
|
|
826
826
|
|
|
827
827
|
|
|
828
|
+
# ---- `sibyl update` ----------------------------------------------------
|
|
829
|
+
|
|
830
|
+
# Three user-facing packages we offer to upgrade. `mcp` is opt-in and not
|
|
831
|
+
# bundled by default — skip it here so we don't tell users to "update"
|
|
832
|
+
# something they may not have installed. Add it back when an `--include-mcp`
|
|
833
|
+
# flag is shipped.
|
|
834
|
+
UPDATE_PACKAGES = ("sibyl-memory-cli", "sibyl-memory-hermes", "sibyl-memory-client")
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _installed_version(pkg: str) -> str | None:
|
|
838
|
+
"""Return the locally-installed version of a package, or None if not installed."""
|
|
839
|
+
try:
|
|
840
|
+
from importlib.metadata import PackageNotFoundError, version as _v
|
|
841
|
+
try:
|
|
842
|
+
return _v(pkg)
|
|
843
|
+
except PackageNotFoundError:
|
|
844
|
+
return None
|
|
845
|
+
except Exception:
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _pypi_latest(pkg: str, timeout: float = 4.0) -> str | None:
|
|
850
|
+
"""Hit PyPI's JSON endpoint for the latest published version. Best-effort."""
|
|
851
|
+
url = f"https://pypi.org/pypi/{pkg}/json"
|
|
852
|
+
try:
|
|
853
|
+
req = urllib.request.Request(url, headers={"User-Agent": f"sibyl-memory-cli/{_client_version()}"})
|
|
854
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
855
|
+
data = json.loads(r.read().decode("utf-8"))
|
|
856
|
+
return (data.get("info") or {}).get("version")
|
|
857
|
+
except Exception:
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _ver_tuple(v: str) -> tuple:
|
|
862
|
+
"""Lenient version tuple for comparison. Splits on '.', tolerates non-numeric tails."""
|
|
863
|
+
out = []
|
|
864
|
+
for part in (v or "").split("."):
|
|
865
|
+
digits = ""
|
|
866
|
+
for ch in part:
|
|
867
|
+
if ch.isdigit():
|
|
868
|
+
digits += ch
|
|
869
|
+
else:
|
|
870
|
+
break
|
|
871
|
+
out.append(int(digits) if digits else 0)
|
|
872
|
+
return tuple(out)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _detect_install_method() -> str:
|
|
876
|
+
"""Best-guess of how the CLI was installed — pipx / venv / system-pip / pep668-blocked."""
|
|
877
|
+
exe = sys.executable
|
|
878
|
+
if "/pipx/" in exe or "/.local/pipx/" in exe:
|
|
879
|
+
return "pipx"
|
|
880
|
+
if exe and ("venv" in exe.lower() or "virtualenv" in exe.lower() or os.environ.get("VIRTUAL_ENV")):
|
|
881
|
+
return "venv"
|
|
882
|
+
# Look for PEP 668 marker file
|
|
883
|
+
for parent in Path(exe).resolve().parents:
|
|
884
|
+
marker = parent / "lib" / "EXTERNALLY-MANAGED"
|
|
885
|
+
if marker.exists():
|
|
886
|
+
return "pep668"
|
|
887
|
+
marker2 = parent / "EXTERNALLY-MANAGED"
|
|
888
|
+
if marker2.exists():
|
|
889
|
+
return "pep668"
|
|
890
|
+
if str(parent) in ("/", "/home", "/usr"):
|
|
891
|
+
break
|
|
892
|
+
return "system"
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def cmd_update(args: argparse.Namespace) -> int:
|
|
896
|
+
"""Check installed package versions against PyPI, optionally apply upgrade."""
|
|
897
|
+
rows = []
|
|
898
|
+
any_outdated = False
|
|
899
|
+
for pkg in UPDATE_PACKAGES:
|
|
900
|
+
installed = _installed_version(pkg)
|
|
901
|
+
latest = _pypi_latest(pkg)
|
|
902
|
+
outdated = False
|
|
903
|
+
if installed and latest:
|
|
904
|
+
outdated = _ver_tuple(installed) < _ver_tuple(latest)
|
|
905
|
+
rows.append({"pkg": pkg, "installed": installed, "latest": latest, "outdated": outdated})
|
|
906
|
+
if outdated:
|
|
907
|
+
any_outdated = True
|
|
908
|
+
|
|
909
|
+
if args.json:
|
|
910
|
+
print(json.dumps({"packages": rows, "any_outdated": any_outdated}, indent=2))
|
|
911
|
+
return 0 if not any_outdated else 2
|
|
912
|
+
|
|
913
|
+
# ASCII output — keep it small and readable, follow `sibyl status` style.
|
|
914
|
+
print()
|
|
915
|
+
if any_outdated:
|
|
916
|
+
print(a.err_line("Updates available."))
|
|
917
|
+
else:
|
|
918
|
+
# Distinguish "all current" from "could not reach PyPI"
|
|
919
|
+
any_unreachable = any(r["latest"] is None for r in rows)
|
|
920
|
+
if any_unreachable:
|
|
921
|
+
print(a.dim("Could not reach PyPI for one or more packages — showing what we know."))
|
|
922
|
+
else:
|
|
923
|
+
print(a.success_line("All packages current."))
|
|
924
|
+
print()
|
|
925
|
+
|
|
926
|
+
name_w = max(len(r["pkg"]) for r in rows)
|
|
927
|
+
for r in rows:
|
|
928
|
+
installed = r["installed"] or "(not installed)"
|
|
929
|
+
latest = r["latest"] or "(unreachable)"
|
|
930
|
+
if r["outdated"]:
|
|
931
|
+
line = f" {yellow(r['pkg'].ljust(name_w))} {installed} → {green(latest)}"
|
|
932
|
+
elif r["installed"] is None:
|
|
933
|
+
line = f" {a.dim(r['pkg'].ljust(name_w))} {a.dim(installed)}"
|
|
934
|
+
else:
|
|
935
|
+
line = f" {r['pkg'].ljust(name_w)} {a.dim(installed)}"
|
|
936
|
+
print(line)
|
|
937
|
+
print()
|
|
938
|
+
|
|
939
|
+
if not any_outdated:
|
|
940
|
+
return 0
|
|
941
|
+
|
|
942
|
+
pip_cmd_pkgs = " ".join(r["pkg"] for r in rows if r["outdated"])
|
|
943
|
+
method = _detect_install_method()
|
|
944
|
+
|
|
945
|
+
if args.apply:
|
|
946
|
+
# Best-effort in-process pip invocation
|
|
947
|
+
import subprocess
|
|
948
|
+
pip_args = [sys.executable, "-m", "pip", "install", "-U", *pip_cmd_pkgs.split()]
|
|
949
|
+
if method == "pep668":
|
|
950
|
+
pip_args.append("--break-system-packages")
|
|
951
|
+
if method == "pipx":
|
|
952
|
+
# pipx is a separate tool; we can't drive it via `pip install`.
|
|
953
|
+
print(a.err_line("Detected pipx install. Run instead:"))
|
|
954
|
+
print(f" pipx upgrade {' '.join(r['pkg'] for r in rows if r['outdated'])}")
|
|
955
|
+
return 2
|
|
956
|
+
print(a.dim("Running: ") + " ".join(pip_args))
|
|
957
|
+
try:
|
|
958
|
+
rc = subprocess.call(pip_args)
|
|
959
|
+
except FileNotFoundError:
|
|
960
|
+
print(a.err_line("pip not found at " + sys.executable + " -m pip"))
|
|
961
|
+
return 2
|
|
962
|
+
if rc == 0:
|
|
963
|
+
print()
|
|
964
|
+
print(a.success_line("Upgrade complete. Re-run `sibyl update` to confirm."))
|
|
965
|
+
return rc
|
|
966
|
+
|
|
967
|
+
# Default: print the command, do not execute
|
|
968
|
+
print(a.dim("To upgrade, run:"))
|
|
969
|
+
if method == "pipx":
|
|
970
|
+
print(f" pipx upgrade {pip_cmd_pkgs}")
|
|
971
|
+
elif method == "pep668":
|
|
972
|
+
print(f" pip install --break-system-packages -U {pip_cmd_pkgs}")
|
|
973
|
+
print()
|
|
974
|
+
print(a.dim(" (Your Python flags itself as externally-managed under PEP 668.)"))
|
|
975
|
+
print(a.dim(" (Cleanest: install inside a venv. See https://beta.sibyllabs.org for the recommended path.)"))
|
|
976
|
+
else:
|
|
977
|
+
print(f" pip install -U {pip_cmd_pkgs}")
|
|
978
|
+
print()
|
|
979
|
+
print(a.dim("Or let sibyl run it:") + " sibyl update --apply")
|
|
980
|
+
print()
|
|
981
|
+
return 2 # exit 2 signals "outdated" without being a hard error
|
|
982
|
+
|
|
983
|
+
|
|
828
984
|
# ---- Dispatch ----------------------------------------------------------
|
|
829
985
|
|
|
830
986
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -871,6 +1027,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
871
1027
|
p_h = sub.add_parser("health", help="Run the provider self-check")
|
|
872
1028
|
p_h.set_defaults(func=cmd_health)
|
|
873
1029
|
|
|
1030
|
+
p_update = sub.add_parser(
|
|
1031
|
+
"update",
|
|
1032
|
+
help="Check for newer sibyl-memory-* releases on PyPI (use --apply to upgrade)",
|
|
1033
|
+
)
|
|
1034
|
+
p_update.add_argument("--apply", action="store_true", help="Run pip install -U for the outdated packages")
|
|
1035
|
+
p_update.add_argument("--json", action="store_true", help="Machine-readable output")
|
|
1036
|
+
p_update.set_defaults(func=cmd_update)
|
|
1037
|
+
|
|
874
1038
|
# v0.1.4: one-command auto-detect-and-wire setup for any agent stack
|
|
875
1039
|
from .setup import cmd_setup
|
|
876
1040
|
p_setup = sub.add_parser(
|
|
@@ -878,7 +1042,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
878
1042
|
help="Auto-detect Hermes / Claude Code and wire SIBYL as the memory provider",
|
|
879
1043
|
)
|
|
880
1044
|
p_setup.add_argument(
|
|
881
|
-
"target", nargs="?", choices=list(["hermes", "claude-code"]),
|
|
1045
|
+
"target", nargs="?", choices=list(["hermes", "claude-code", "codex"]),
|
|
882
1046
|
help="Wire just this framework (default: detect all)",
|
|
883
1047
|
)
|
|
884
1048
|
p_setup.add_argument(
|
|
@@ -899,7 +1063,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
899
1063
|
)
|
|
900
1064
|
p_setup.add_argument(
|
|
901
1065
|
"--claude-settings", default=None,
|
|
902
|
-
help="Override ~/.claude
|
|
1066
|
+
help="Override ~/.claude.json autodetection",
|
|
1067
|
+
)
|
|
1068
|
+
p_setup.add_argument(
|
|
1069
|
+
"--codex-config", default=None,
|
|
1070
|
+
help="Override ~/.codex/config.toml autodetection",
|
|
903
1071
|
)
|
|
904
1072
|
p_setup.set_defaults(func=cmd_setup)
|
|
905
1073
|
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
Maximum-efficiency onboarding command. Single-command path for the user:
|
|
4
4
|
|
|
5
5
|
pip install sibyl-memory-cli
|
|
6
|
-
sibyl setup # auto-detects Hermes + Claude Code, prompts per stack, wires
|
|
6
|
+
sibyl setup # auto-detects Hermes + Claude Code + Codex, prompts per stack, wires
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Three wirers in v0.3.7:
|
|
9
9
|
- HermesWirer: install-plugin + edit $HERMES_HOME/config.yaml (memory.provider)
|
|
10
|
-
- ClaudeCodeWirer: edit ~/.claude
|
|
10
|
+
- ClaudeCodeWirer: edit ~/.claude.json (mcpServers.sibyl-memory)
|
|
11
|
+
- CodexWirer: edit ~/.codex/config.toml ([mcp_servers.sibyl_memory])
|
|
11
12
|
|
|
12
13
|
Each wirer follows the same protocol:
|
|
13
14
|
is_present() -> bool (filesystem + PATH detect)
|
|
@@ -26,6 +27,7 @@ from __future__ import annotations
|
|
|
26
27
|
import argparse
|
|
27
28
|
import json
|
|
28
29
|
import os
|
|
30
|
+
import re
|
|
29
31
|
import shutil
|
|
30
32
|
import sys
|
|
31
33
|
from dataclasses import dataclass
|
|
@@ -229,9 +231,15 @@ class ClaudeCodeWirer:
|
|
|
229
231
|
SIBYL_MCP_BLOCK = {"command": "sibyl-memory-mcp"}
|
|
230
232
|
|
|
231
233
|
def __init__(self, *, settings_path: Optional[Union[str, Path]] = None):
|
|
234
|
+
# v0.3.7 (2026-05-22): default flipped from ~/.claude/settings.json to
|
|
235
|
+
# ~/.claude.json. Recent Claude Code reads MCP server config from the
|
|
236
|
+
# user-level ~/.claude.json — writing to ~/.claude/settings.json was a
|
|
237
|
+
# silent-failure path (wirer reported success but MCP server didn't
|
|
238
|
+
# appear in Claude Code). Override with --claude-settings if your
|
|
239
|
+
# install uses a non-standard location.
|
|
232
240
|
self.settings_path = (
|
|
233
241
|
Path(settings_path).expanduser() if settings_path
|
|
234
|
-
else Path.home() / ".claude
|
|
242
|
+
else Path.home() / ".claude.json"
|
|
235
243
|
)
|
|
236
244
|
|
|
237
245
|
def is_present(self) -> bool:
|
|
@@ -333,6 +341,158 @@ class ClaudeCodeWirer:
|
|
|
333
341
|
os.replace(tmp, self.settings_path)
|
|
334
342
|
|
|
335
343
|
|
|
344
|
+
# ----------------------------------------------------------------------
|
|
345
|
+
# CodexWirer — edits ~/.codex/config.toml
|
|
346
|
+
# ----------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
class CodexWirer:
|
|
349
|
+
"""Auto-wire Codex CLI's MCP server registry at ~/.codex/config.toml.
|
|
350
|
+
|
|
351
|
+
Codex reads server defs from a TOML table named `[mcp_servers.<name>]`.
|
|
352
|
+
We use `[mcp_servers.sibyl_memory]` (snake_case is TOML-idiomatic; the
|
|
353
|
+
Claude Code side uses `sibyl-memory` because JSON keys allow hyphens
|
|
354
|
+
and Anthropic docs use that form).
|
|
355
|
+
|
|
356
|
+
Idempotency is text-based rather than full TOML round-trip, so we
|
|
357
|
+
preserve operator hand-edits and comments in the rest of the file
|
|
358
|
+
instead of reformatting on every run. The cost: we won't catch
|
|
359
|
+
pathological TOML edge cases (e.g., comments mid-key). For the
|
|
360
|
+
canonical Codex config layout this is fine.
|
|
361
|
+
"""
|
|
362
|
+
name = "codex"
|
|
363
|
+
display_name = "Codex CLI"
|
|
364
|
+
initial = "x" # 'c' is taken by Claude Code; 'x' from codeX
|
|
365
|
+
|
|
366
|
+
SIBYL_TOML_BLOCK = '[mcp_servers.sibyl_memory]\ncommand = "sibyl-memory-mcp"\n'
|
|
367
|
+
BLOCK_HEADER_RE = re.compile(
|
|
368
|
+
r'^\s*\[\s*mcp_servers\s*\.\s*sibyl_memory\s*\]\s*$', re.MULTILINE
|
|
369
|
+
)
|
|
370
|
+
NEXT_TABLE_RE = re.compile(r'^\s*\[', re.MULTILINE)
|
|
371
|
+
SIBYL_COMMAND_RE = re.compile(
|
|
372
|
+
r'^\s*command\s*=\s*"sibyl-memory-mcp"\s*$', re.MULTILINE
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def __init__(self, *, config_path: Optional[Union[str, Path]] = None):
|
|
376
|
+
self.config_path = (
|
|
377
|
+
Path(config_path).expanduser() if config_path
|
|
378
|
+
else Path.home() / ".codex" / "config.toml"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def is_present(self) -> bool:
|
|
382
|
+
if self.config_path.exists():
|
|
383
|
+
return True
|
|
384
|
+
if shutil.which("codex"):
|
|
385
|
+
return True
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
def _read(self) -> str:
|
|
389
|
+
if not self.config_path.exists():
|
|
390
|
+
return ""
|
|
391
|
+
try:
|
|
392
|
+
return self.config_path.read_text(encoding="utf-8")
|
|
393
|
+
except Exception:
|
|
394
|
+
return ""
|
|
395
|
+
|
|
396
|
+
def _extract_block(self, content: str) -> Optional[str]:
|
|
397
|
+
m = self.BLOCK_HEADER_RE.search(content)
|
|
398
|
+
if not m:
|
|
399
|
+
return None
|
|
400
|
+
# Find next [...] table header after our match (start-of-line "[").
|
|
401
|
+
next_tbl = self.NEXT_TABLE_RE.search(content, m.end())
|
|
402
|
+
end = next_tbl.start() if next_tbl else len(content)
|
|
403
|
+
return content[m.start():end]
|
|
404
|
+
|
|
405
|
+
def current_state(self) -> dict:
|
|
406
|
+
content = self._read()
|
|
407
|
+
block = self._extract_block(content)
|
|
408
|
+
sibyl_block_present = block is not None
|
|
409
|
+
wired_with_sibyl = bool(block and self.SIBYL_COMMAND_RE.search(block))
|
|
410
|
+
return {
|
|
411
|
+
"config_path": str(self.config_path),
|
|
412
|
+
"config_exists": self.config_path.exists(),
|
|
413
|
+
"sibyl_block_present": sibyl_block_present,
|
|
414
|
+
"wired_with_sibyl": wired_with_sibyl,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
def wire(self, *, force: bool = False, dry_run: bool = False,
|
|
418
|
+
prompt_fn: Optional[Callable[..., str]] = None) -> WireOutcome:
|
|
419
|
+
state = self.current_state()
|
|
420
|
+
|
|
421
|
+
if state["wired_with_sibyl"]:
|
|
422
|
+
return WireOutcome(
|
|
423
|
+
self.name, "already",
|
|
424
|
+
f"Codex CLI already has [mcp_servers.sibyl_memory] block in {self.config_path}",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if state["sibyl_block_present"] and not force:
|
|
428
|
+
if prompt_fn is None:
|
|
429
|
+
return WireOutcome(
|
|
430
|
+
self.name, "skipped",
|
|
431
|
+
"Existing [mcp_servers.sibyl_memory] differs. Use --force to overwrite.",
|
|
432
|
+
)
|
|
433
|
+
ans = prompt_fn(
|
|
434
|
+
"Codex has [mcp_servers.sibyl_memory] but command differs. Update?",
|
|
435
|
+
default="N",
|
|
436
|
+
)
|
|
437
|
+
if ans != "y":
|
|
438
|
+
return WireOutcome(self.name, "skipped", "TOML block update declined.")
|
|
439
|
+
|
|
440
|
+
if dry_run:
|
|
441
|
+
verb = "replace" if state["sibyl_block_present"] else "append"
|
|
442
|
+
return WireOutcome(
|
|
443
|
+
self.name, "dry-run",
|
|
444
|
+
f"Would {verb} [mcp_servers.sibyl_memory] in {self.config_path}",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
backup = self._backup_config()
|
|
448
|
+
try:
|
|
449
|
+
self._write_config_with_sibyl(replace_existing=state["sibyl_block_present"])
|
|
450
|
+
except Exception as e:
|
|
451
|
+
return WireOutcome(
|
|
452
|
+
self.name, "error",
|
|
453
|
+
f"config write failed: {type(e).__name__}: {e}",
|
|
454
|
+
backup_path=backup,
|
|
455
|
+
)
|
|
456
|
+
return WireOutcome(
|
|
457
|
+
self.name, "wired",
|
|
458
|
+
f"Added [mcp_servers.sibyl_memory] to {self.config_path}",
|
|
459
|
+
backup_path=backup,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def _backup_config(self) -> Optional[Path]:
|
|
463
|
+
if not self.config_path.exists():
|
|
464
|
+
return None
|
|
465
|
+
backup = self.config_path.with_suffix(".toml.bak")
|
|
466
|
+
shutil.copy2(self.config_path, backup)
|
|
467
|
+
return backup
|
|
468
|
+
|
|
469
|
+
def _write_config_with_sibyl(self, *, replace_existing: bool) -> None:
|
|
470
|
+
content = self._read()
|
|
471
|
+
|
|
472
|
+
if replace_existing:
|
|
473
|
+
existing = self._extract_block(content) or ""
|
|
474
|
+
# Rstrip the existing block to a single newline before splice so
|
|
475
|
+
# we don't accumulate blank lines on repeat replaces.
|
|
476
|
+
stripped = existing.rstrip("\n")
|
|
477
|
+
new_content = content.replace(stripped, self.SIBYL_TOML_BLOCK.rstrip("\n"), 1)
|
|
478
|
+
# Ensure file ends with a newline
|
|
479
|
+
if not new_content.endswith("\n"):
|
|
480
|
+
new_content += "\n"
|
|
481
|
+
else:
|
|
482
|
+
# Append: one blank line of separator if file is non-empty
|
|
483
|
+
if not content:
|
|
484
|
+
new_content = self.SIBYL_TOML_BLOCK
|
|
485
|
+
else:
|
|
486
|
+
# Normalize trailing whitespace to exactly "\n\n"
|
|
487
|
+
trimmed = content.rstrip("\n") + "\n\n"
|
|
488
|
+
new_content = trimmed + self.SIBYL_TOML_BLOCK
|
|
489
|
+
|
|
490
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
491
|
+
tmp = self.config_path.with_suffix(".toml.tmp")
|
|
492
|
+
tmp.write_text(new_content, encoding="utf-8")
|
|
493
|
+
os.replace(tmp, self.config_path)
|
|
494
|
+
|
|
495
|
+
|
|
336
496
|
# ----------------------------------------------------------------------
|
|
337
497
|
# Registry + dispatch
|
|
338
498
|
# ----------------------------------------------------------------------
|
|
@@ -340,6 +500,7 @@ class ClaudeCodeWirer:
|
|
|
340
500
|
ALL_WIRERS: dict = {
|
|
341
501
|
"hermes": HermesWirer,
|
|
342
502
|
"claude-code": ClaudeCodeWirer,
|
|
503
|
+
"codex": CodexWirer,
|
|
343
504
|
}
|
|
344
505
|
|
|
345
506
|
|
|
@@ -366,6 +527,8 @@ def _wirer_kwargs(args: argparse.Namespace, name: str) -> dict:
|
|
|
366
527
|
kw["hermes_home"] = args.hermes_home
|
|
367
528
|
if name == "claude-code" and getattr(args, "claude_settings", None):
|
|
368
529
|
kw["settings_path"] = args.claude_settings
|
|
530
|
+
if name == "codex" and getattr(args, "codex_config", None):
|
|
531
|
+
kw["config_path"] = args.codex_config
|
|
369
532
|
return kw
|
|
370
533
|
|
|
371
534
|
|
|
@@ -402,12 +565,13 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
402
565
|
print(dim("Looked for:"))
|
|
403
566
|
for name, w in wirers.items():
|
|
404
567
|
st = w.current_state()
|
|
405
|
-
loc = st.get("hermes_home") or st.get("settings_path")
|
|
568
|
+
loc = st.get("hermes_home") or st.get("settings_path") or st.get("config_path")
|
|
406
569
|
print(f" {w.display_name}: {loc}")
|
|
407
570
|
print()
|
|
408
571
|
print(dim("To override detection, point setup at a custom path:"))
|
|
409
572
|
print(f" {cyan('sibyl setup --hermes-home /custom/path')}")
|
|
410
|
-
print(f" {cyan('sibyl setup --claude-settings /custom
|
|
573
|
+
print(f" {cyan('sibyl setup --claude-settings /custom/.claude.json')}")
|
|
574
|
+
print(f" {cyan('sibyl setup --codex-config /custom/config.toml')}")
|
|
411
575
|
print()
|
|
412
576
|
return 0
|
|
413
577
|
|
|
@@ -415,7 +579,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
415
579
|
print(dim("Detected:"))
|
|
416
580
|
for name, w in detected.items():
|
|
417
581
|
st = w.current_state()
|
|
418
|
-
loc = st.get("hermes_home") or st.get("settings_path")
|
|
582
|
+
loc = st.get("hermes_home") or st.get("settings_path") or st.get("config_path")
|
|
419
583
|
print(f" {w.display_name} at {loc}")
|
|
420
584
|
print()
|
|
421
585
|
|
|
@@ -449,11 +613,14 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
449
613
|
|
|
450
614
|
# Pre-prompt for fresh adds (interactive only). Already-wired and
|
|
451
615
|
# existing-other-provider are handled inside wire() itself.
|
|
616
|
+
# State keys per wirer: hermes → memory_provider; claude-code →
|
|
617
|
+
# sibyl_mcp; codex → sibyl_block_present.
|
|
452
618
|
if (
|
|
453
619
|
not args.yes
|
|
454
620
|
and not st.get("wired_with_sibyl")
|
|
455
621
|
and not st.get("memory_provider")
|
|
456
622
|
and not st.get("sibyl_mcp")
|
|
623
|
+
and not st.get("sibyl_block_present")
|
|
457
624
|
):
|
|
458
625
|
if name == "hermes":
|
|
459
626
|
q = f"Set SIBYL as default memory provider in {wirer.display_name}?"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
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
|
|
@@ -48,39 +48,37 @@ sibyl health Run the SibylMemoryProvider self-check (schema versi
|
|
|
48
48
|
```bash
|
|
49
49
|
$ sibyl init
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Sibyl Memory Plugin · activation
|
|
52
52
|
|
|
53
|
-
Session
|
|
54
|
-
|
|
55
|
-
Opening https://auth.sibyllabs.org/a1b2c3d4-…
|
|
53
|
+
Session: a1b2c3d4…e5f6
|
|
54
|
+
Opening: https://sibyllabs.org/plugin/activate?session=a1b2c3d4-…
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
this terminal will pick up automatically when you bind.
|
|
56
|
+
Sign in with your wallet in the browser. This terminal will pick up automatically.
|
|
59
57
|
|
|
60
|
-
⠹
|
|
58
|
+
⠹ waiting for browser activation … 9:42 left
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
The browser opens
|
|
61
|
+
The browser opens. Sign a SIWE message with your wallet. The terminal picks up the moment the binding lands. Credentials are written to `~/.sibyl-memory/credentials.json` at mode 0600.
|
|
64
62
|
|
|
65
63
|
## Upgrade
|
|
66
64
|
|
|
67
65
|
```bash
|
|
68
66
|
$ sibyl upgrade
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Sibyl Memory Plugin · upgrade
|
|
71
69
|
|
|
72
70
|
Account a1b2c3d4…e5f6
|
|
73
71
|
Current tier FREE
|
|
74
72
|
Opening https://sibyllabs.org/plugin/upgrade?session=…
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
74
|
+
Two paths in your browser:
|
|
75
|
+
1. Stake $SIBYL on Base (free unlimited if you qualify)
|
|
76
|
+
2. Subscribe in USDC (monthly / quarterly / annual)
|
|
79
77
|
```
|
|
80
78
|
|
|
81
79
|
In the browser:
|
|
82
|
-
- **Stake
|
|
83
|
-
- **Subscribe
|
|
80
|
+
- **Stake**: connect your wallet (browser or Coinbase Smart Wallet), sign to bind, and the page checks your `$SIBYL` balance on Base. If you hold the threshold (default 100,000 $SIBYL liquid+staked, configurable), the local cap lifts.
|
|
81
|
+
- **Subscribe**: pick monthly ($29) / quarterly ($79) / annual ($290) USDC, sign the transfer, the server records the subscription. Tier flips immediately.
|
|
84
82
|
|
|
85
83
|
On either path, the CLI sees the tier change, rewrites `credentials.json`, and clears `tier_cache.json` so your next write picks up the new entitlement without delay.
|
|
86
84
|
|
|
@@ -115,16 +113,14 @@ For internal testing only:
|
|
|
115
113
|
|
|
116
114
|
```bash
|
|
117
115
|
SIBYL_API_BASE=https://staging.api.sibyllabs.org sibyl init
|
|
118
|
-
SIBYL_ACTIVATE_BASE=https://staging.
|
|
116
|
+
SIBYL_ACTIVATE_BASE=https://staging.sibyllabs.org/plugin/activate sibyl init
|
|
119
117
|
SIBYL_UPGRADE_BASE=https://staging.sibyllabs.org/plugin/upgrade sibyl upgrade
|
|
120
118
|
```
|
|
121
119
|
|
|
122
|
-
`SIBYL_ACTIVATE_BASE` accepts both subdomain form (`https://auth.example.org` → path-based session URL: `https://auth.example.org/<uuid>`) and legacy path form (`https://example.org/plugin/activate` → query-based: `?session=<uuid>`). The CLI auto-detects from the hostname.
|
|
123
|
-
|
|
124
120
|
## Security
|
|
125
121
|
|
|
126
122
|
- `credentials.json` is written atomically at mode 0600.
|
|
127
|
-
- `session_token` is never printed in full
|
|
123
|
+
- `session_token` is never printed in full: only a short slice.
|
|
128
124
|
- No memory content ever transits these endpoints. The CLI never reads `memory.db` content; it only checks file size.
|
|
129
125
|
- Wallet operations happen in the browser. The CLI sees only the resulting tier change.
|
|
130
126
|
|
|
@@ -36,7 +36,7 @@ from sibyl_memory_cli.setup import ( # noqa: E402
|
|
|
36
36
|
# ----------------------------------------------------------------------
|
|
37
37
|
|
|
38
38
|
def _stub_install_plugin(hermes_home: str):
|
|
39
|
-
"""Replacement for sibyl_memory_hermes.install_plugin.install
|
|
39
|
+
"""Replacement for sibyl_memory_hermes.install_plugin.install: drops a fake
|
|
40
40
|
adapter file so the wirer sees plugin_installed=True afterwards."""
|
|
41
41
|
plugin_dir = Path(hermes_home) / "plugins" / "sibyl"
|
|
42
42
|
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.3.5 → sibyl_memory_cli-0.3.7}/src/sibyl_memory_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|