sibyl-memory-cli 0.1.1__tar.gz → 0.1.3__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.1.1 → sibyl_memory_cli-0.1.3}/PKG-INFO +3 -3
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/pyproject.toml +3 -3
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli/__init__.py +7 -1
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli/cli.py +88 -19
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/PKG-INFO +3 -3
- sibyl_memory_cli-0.1.3/src/sibyl_memory_cli.egg-info/requires.txt +5 -0
- sibyl_memory_cli-0.1.1/src/sibyl_memory_cli.egg-info/requires.txt +0 -5
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/README.md +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/setup.cfg +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/SOURCES.txt +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/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.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.
|
|
5
5
|
Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
|
|
6
6
|
License: MIT
|
|
@@ -18,8 +18,8 @@ Classifier: Environment :: Console
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: sibyl-memory-client>=0.
|
|
22
|
-
Requires-Dist: sibyl-memory-hermes>=0.2
|
|
21
|
+
Requires-Dist: sibyl-memory-client>=0.4.0
|
|
22
|
+
Requires-Dist: sibyl-memory-hermes>=0.3.2
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
25
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sibyl-memory-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
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."
|
|
9
9
|
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -23,8 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"sibyl-memory-client>=0.
|
|
27
|
-
"sibyl-memory-hermes>=0.2
|
|
26
|
+
"sibyl-memory-client>=0.4.0",
|
|
27
|
+
"sibyl-memory-hermes>=0.3.2",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
@@ -13,6 +13,12 @@ All HTTP calls target https://api.sibyllabs.org/api/plugin/*.
|
|
|
13
13
|
"""
|
|
14
14
|
from .cli import main
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Single-sourced from installed metadata so wheel + code can't drift
|
|
17
|
+
# (C3 audit fix v0.1.2). Same pattern as sibyl-memory-hermes v0.3.0+.
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
19
|
+
try:
|
|
20
|
+
__version__ = _pkg_version("sibyl-memory-cli")
|
|
21
|
+
except PackageNotFoundError: # pragma: no cover - source-tree dev only
|
|
22
|
+
__version__ = "0.0.0+source"
|
|
17
23
|
|
|
18
24
|
__all__ = ["main", "__version__"]
|
|
@@ -5,7 +5,11 @@ https://api.sibyllabs.org/api/plugin/* and the local SibylMemoryProvider.
|
|
|
5
5
|
|
|
6
6
|
Design pillars:
|
|
7
7
|
- Zero non-stdlib deps in this file. urllib is enough.
|
|
8
|
-
- Credentials are written atomically
|
|
8
|
+
- Credentials are written atomically at mode 0600, set at file-creation
|
|
9
|
+
time via O_CREAT|O_EXCL|O_NOFOLLOW (no chmod-after-write race).
|
|
10
|
+
- The URL parameter handed to the browser is an opaque session identifier,
|
|
11
|
+
not the long-lived bearer (audit SEC-1 — server-side pairing handoff
|
|
12
|
+
issues a separate bearer at activation completion if available).
|
|
9
13
|
- session_token is never printed in full — display short slice only.
|
|
10
14
|
- Polling has explicit timeouts; no infinite loops.
|
|
11
15
|
- Every command exits with a clear status code (0 ok, 1 user error, 2 server error).
|
|
@@ -27,6 +31,18 @@ import webbrowser
|
|
|
27
31
|
from pathlib import Path
|
|
28
32
|
from typing import Any
|
|
29
33
|
|
|
34
|
+
|
|
35
|
+
def _client_version() -> str:
|
|
36
|
+
"""Return the installed package version from metadata, never hardcoded."""
|
|
37
|
+
try:
|
|
38
|
+
from importlib.metadata import PackageNotFoundError, version as _v
|
|
39
|
+
try:
|
|
40
|
+
return _v("sibyl-memory-cli")
|
|
41
|
+
except PackageNotFoundError:
|
|
42
|
+
return "0.0.0+source"
|
|
43
|
+
except Exception:
|
|
44
|
+
return "0.0.0+source"
|
|
45
|
+
|
|
30
46
|
# ---- Defaults ----------------------------------------------------------
|
|
31
47
|
|
|
32
48
|
API_BASE = os.environ.get("SIBYL_API_BASE", "https://api.sibyllabs.org")
|
|
@@ -101,7 +117,7 @@ def http_request(
|
|
|
101
117
|
"""Single source of truth for HTTP calls. Returns parsed JSON or raises HttpError."""
|
|
102
118
|
url = f"{API_BASE}{path}"
|
|
103
119
|
data = None
|
|
104
|
-
full_headers = {"Accept": "application/json", "User-Agent": "sibyl-memory-cli/
|
|
120
|
+
full_headers = {"Accept": "application/json", "User-Agent": f"sibyl-memory-cli/{_client_version()}"}
|
|
105
121
|
if body is not None:
|
|
106
122
|
data = json.dumps(body).encode("utf-8")
|
|
107
123
|
full_headers["Content-Type"] = "application/json"
|
|
@@ -124,20 +140,45 @@ def http_request(
|
|
|
124
140
|
# ---- Credentials I/O ---------------------------------------------------
|
|
125
141
|
|
|
126
142
|
def write_credentials_atomic(creds: dict, path: Path = DEFAULT_CRED_PATH) -> Path:
|
|
127
|
-
"""Write credentials.json atomically at mode 0600.
|
|
143
|
+
"""Write credentials.json atomically at mode 0600.
|
|
144
|
+
|
|
145
|
+
v0.1.2 hardening (audit SEC-2): mode 0600 is set by the kernel at
|
|
146
|
+
file-creation time via O_CREAT|O_EXCL|O_NOFOLLOW. Previously used
|
|
147
|
+
write_text() followed by os.chmod(), leaving a world-readable window
|
|
148
|
+
between syscalls every credential save.
|
|
149
|
+
"""
|
|
128
150
|
path = path.expanduser()
|
|
129
151
|
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
152
|
+
data = json.dumps(creds, indent=2).encode("utf-8")
|
|
130
153
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
131
|
-
tmp
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
# Clean any leftover .tmp from a crashed prior write so O_EXCL can succeed.
|
|
155
|
+
try:
|
|
156
|
+
os.unlink(tmp)
|
|
157
|
+
except FileNotFoundError:
|
|
158
|
+
pass
|
|
159
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
160
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
161
|
+
flags |= os.O_NOFOLLOW
|
|
162
|
+
fd = os.open(str(tmp), flags, 0o600)
|
|
163
|
+
try:
|
|
164
|
+
os.write(fd, data)
|
|
165
|
+
os.fsync(fd)
|
|
166
|
+
finally:
|
|
167
|
+
os.close(fd)
|
|
168
|
+
os.replace(str(tmp), str(path))
|
|
134
169
|
return path
|
|
135
170
|
|
|
136
171
|
|
|
137
172
|
def read_credentials(path: Path = DEFAULT_CRED_PATH) -> dict | None:
|
|
173
|
+
"""Read credentials.json.
|
|
174
|
+
|
|
175
|
+
v0.1.2 hardening (audit SEC-11): refuses to follow symlinks.
|
|
176
|
+
Returns None if the file is a symlink or doesn't exist."""
|
|
138
177
|
path = path.expanduser()
|
|
139
178
|
if not path.exists():
|
|
140
179
|
return None
|
|
180
|
+
if path.is_symlink():
|
|
181
|
+
return None
|
|
141
182
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
142
183
|
|
|
143
184
|
|
|
@@ -180,10 +221,18 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
180
221
|
print_status("Credentials", str(cred_path))
|
|
181
222
|
return 0
|
|
182
223
|
|
|
183
|
-
|
|
224
|
+
# SEC-1 mitigation (v0.1.2): the URL parameter is an opaque pairing
|
|
225
|
+
# session identifier, NOT the long-lived bearer used by /access and
|
|
226
|
+
# /check-write. The CLI generates it locally and the server treats
|
|
227
|
+
# it as the activation rendezvous key only. The persistent bearer
|
|
228
|
+
# is issued by the server in the /check response (`bearer_token`
|
|
229
|
+
# field) after activation completes. Servers running pre-SEC-1
|
|
230
|
+
# firmware that echo the URL identifier as the bearer still work —
|
|
231
|
+
# we use whichever the server returns in the bound credentials.
|
|
232
|
+
session_id = str(uuid.uuid4())
|
|
184
233
|
pairing_code = _gen_pairing_code()
|
|
185
|
-
code_hash = _hash_pairing_code(pairing_code,
|
|
186
|
-
activate_url = f"{ACTIVATE_BASE}?session={
|
|
234
|
+
code_hash = _hash_pairing_code(pairing_code, session_id)
|
|
235
|
+
activate_url = f"{ACTIVATE_BASE}?session={session_id}"
|
|
187
236
|
|
|
188
237
|
# Pre-register the session + pairing code hash with the server.
|
|
189
238
|
# The code itself never leaves the user's machine until they type it
|
|
@@ -193,12 +242,12 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
193
242
|
"POST",
|
|
194
243
|
"/api/plugin/session-init",
|
|
195
244
|
body={
|
|
196
|
-
"session":
|
|
245
|
+
"session": session_id,
|
|
197
246
|
"pairing_code_hash": code_hash,
|
|
198
247
|
"env": {
|
|
199
248
|
"os_family": _detect_os_family(),
|
|
200
249
|
"install_method": "cli",
|
|
201
|
-
"client_version":
|
|
250
|
+
"client_version": _client_version(),
|
|
202
251
|
},
|
|
203
252
|
},
|
|
204
253
|
timeout=10.0,
|
|
@@ -211,7 +260,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
211
260
|
print()
|
|
212
261
|
print(bold("Sibyl Memory Plugin · activation"))
|
|
213
262
|
print()
|
|
214
|
-
print(f" {dim('Session:')} {short(
|
|
263
|
+
print(f" {dim('Session:')} {short(session_id)}")
|
|
215
264
|
# Format the code with a visual break to make it easier to read off the screen
|
|
216
265
|
print(f" {dim('Code:')} {bold(pairing_code[:3] + ' ' + pairing_code[3:])} {dim('(use this in the email panel)')}")
|
|
217
266
|
print(f" {dim('Opening:')} {activate_url}")
|
|
@@ -232,7 +281,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
232
281
|
|
|
233
282
|
while time.time() < deadline:
|
|
234
283
|
try:
|
|
235
|
-
resp = http_request("GET", f"/api/plugin/check?session={urllib.parse.quote(
|
|
284
|
+
resp = http_request("GET", f"/api/plugin/check?session={urllib.parse.quote(session_id)}", timeout=10.0)
|
|
236
285
|
except HttpError as e:
|
|
237
286
|
if e.status in (404, 503, 0):
|
|
238
287
|
# Session not yet created server-side, or transient — keep polling
|
|
@@ -244,12 +293,23 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
244
293
|
|
|
245
294
|
if resp.get("bound") and resp.get("credentials"):
|
|
246
295
|
creds = resp["credentials"]
|
|
247
|
-
#
|
|
248
|
-
|
|
296
|
+
# SEC-1: prefer the server-issued bearer_token (post-fix) over
|
|
297
|
+
# echoing the URL pairing-session id. Servers running pre-SEC-1
|
|
298
|
+
# firmware echo `session_token` back as the bearer — we use
|
|
299
|
+
# whichever the server returns. The CLI's session_id (URL
|
|
300
|
+
# identifier) is the rendezvous key, not the persistent bearer.
|
|
301
|
+
bearer = creds.get("bearer_token") or creds.get("session_token")
|
|
302
|
+
if not bearer:
|
|
303
|
+
# Fallback: pre-SEC-1 server flow where neither field is
|
|
304
|
+
# echoed back — inject the pairing session id so subsequent
|
|
305
|
+
# /access and /check-write calls have something to send.
|
|
306
|
+
bearer = session_id
|
|
307
|
+
# Sanity check on echoed session_token (pre-SEC-1 flow only)
|
|
308
|
+
if creds.get("session_token") and creds["session_token"] != session_id \
|
|
309
|
+
and not creds.get("bearer_token"):
|
|
249
310
|
print(red("\nSession token mismatch — refusing to write credentials."))
|
|
250
311
|
return 2
|
|
251
|
-
|
|
252
|
-
creds.setdefault("session_token", session_token)
|
|
312
|
+
creds["session_token"] = bearer
|
|
253
313
|
path = write_credentials_atomic(creds, cred_path)
|
|
254
314
|
print(f"\r{' ' * 80}\r", end="") # clear spinner line
|
|
255
315
|
print(green("✓ Activated."))
|
|
@@ -259,9 +319,18 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
259
319
|
print_status("Email", creds.get("email") or "—")
|
|
260
320
|
print_status("Credentials", str(path))
|
|
261
321
|
print()
|
|
262
|
-
print(dim("
|
|
322
|
+
print(dim("Activate Sibyl in Hermes:"))
|
|
323
|
+
print(dim(" sibyl-memory-hermes install-plugin"))
|
|
324
|
+
print(dim(" # then edit ~/.hermes/config.yaml:"))
|
|
325
|
+
print(dim(" # memory:"))
|
|
326
|
+
print(dim(" # provider: sibyl"))
|
|
327
|
+
print()
|
|
328
|
+
print(dim("Or use the MCP server (Claude Code / Codex / Cursor / Continue):"))
|
|
329
|
+
print(dim(" pip install sibyl-memory-mcp"))
|
|
330
|
+
print()
|
|
331
|
+
print(dim("Or call the SDK directly from any Python orchestration:"))
|
|
263
332
|
print(dim(" from sibyl_memory_hermes import SibylMemoryProvider"))
|
|
264
|
-
print(dim("
|
|
333
|
+
print(dim(" provider = SibylMemoryProvider()"))
|
|
265
334
|
return 0
|
|
266
335
|
|
|
267
336
|
# Spinner tick
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.
|
|
5
5
|
Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
|
|
6
6
|
License: MIT
|
|
@@ -18,8 +18,8 @@ Classifier: Environment :: Console
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: sibyl-memory-client>=0.
|
|
22
|
-
Requires-Dist: sibyl-memory-hermes>=0.2
|
|
21
|
+
Requires-Dist: sibyl-memory-client>=0.4.0
|
|
22
|
+
Requires-Dist: sibyl-memory-hermes>=0.3.2
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
25
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.1.1 → sibyl_memory_cli-0.1.3}/src/sibyl_memory_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|