sibyl-memory-cli 0.1.4__tar.gz → 0.3.1__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.4 → sibyl_memory_cli-0.3.1}/PKG-INFO +2 -2
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/pyproject.toml +2 -2
- sibyl_memory_cli-0.3.1/src/sibyl_memory_cli/_aesthetic.py +279 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli/cli.py +285 -83
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/PKG-INFO +2 -2
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/SOURCES.txt +1 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/README.md +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/setup.cfg +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli/__init__.py +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli/setup.py +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/requires.txt +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
- {sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/tests/test_setup.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.1
|
|
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.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://sibyllabs.org/plugin
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sibyl-memory-cli"
|
|
7
|
-
version = "0.1
|
|
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."
|
|
7
|
+
version = "0.3.1"
|
|
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" }
|
|
11
11
|
readme = "README.md"
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Shared visual identity for the sibyl CLI surface.
|
|
2
|
+
|
|
3
|
+
Sister module to `_banner.py`. Where the banner is the identity-reveal
|
|
4
|
+
moment for `sibyl init`, this module supplies the granular building
|
|
5
|
+
blocks every subcommand uses to share one coherent look:
|
|
6
|
+
|
|
7
|
+
- 24-bit-truecolor → 256-color → plain-text degradation cascade
|
|
8
|
+
- Brand palette derived from the lab creme paper face (rule 46)
|
|
9
|
+
- Letter-spaced eyebrow labels, gradient titles, ASCII rule dividers
|
|
10
|
+
- Key/value rows, status chips, success/warn/error glyphs
|
|
11
|
+
- Pulsing accents for live states (activation, upgrade, watching)
|
|
12
|
+
|
|
13
|
+
Voice constraint: precise, editorial, restrained. Gradients flow over
|
|
14
|
+
2–3 stops max. No rainbow. The terminal is paper.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Iterable
|
|
21
|
+
|
|
22
|
+
# ─── Palette (RGB · derived from rule 46 creme-paper tokens) ─────────
|
|
23
|
+
# Names map 1:1 to CSS custom properties on lab artifacts.
|
|
24
|
+
|
|
25
|
+
PAPER = (245, 241, 230) # --paper — foreground accent on dark
|
|
26
|
+
PAPER_DEEP = (237, 230, 211) # --paper-deep — depth on creme
|
|
27
|
+
CARD = (253, 251, 245) # --card — slightly lifted creme
|
|
28
|
+
INK = (21, 17, 10) # --ink — main text on creme
|
|
29
|
+
INK_SOFT = (44, 39, 29) # --ink-soft — body text
|
|
30
|
+
INK_MUTE = (106, 99, 86) # --ink-mute — secondary text
|
|
31
|
+
INK_FAINT = (152, 145, 127) # --ink-faint — tertiary text
|
|
32
|
+
RULE = (216, 208, 187) # --rule — hairline
|
|
33
|
+
RULE_STRONG = (184, 174, 147) # --rule-strong — emphasised hairline
|
|
34
|
+
ACCENT = (138, 106, 42) # --accent — ochre highlight
|
|
35
|
+
ACCENT_WARM = (160, 132, 56) # --accent-warm — softer ochre
|
|
36
|
+
ACCENT_GOLD = (224, 194, 119) # mid gold — gradient bridge
|
|
37
|
+
ACCENT_PALE = (244, 229, 184) # pale gold — gradient top
|
|
38
|
+
JADE = (45, 110, 106) # --jade — cool counterpoint
|
|
39
|
+
PULSE = (29, 138, 130) # --pulse — brighter jade (live signal)
|
|
40
|
+
ERROR = (162, 58, 42) # --error — measured red
|
|
41
|
+
|
|
42
|
+
# Status glyphs (Unicode, terminal-safe in modern fonts)
|
|
43
|
+
GLYPH_OK = "✓"
|
|
44
|
+
GLYPH_WARN = "⚠"
|
|
45
|
+
GLYPH_ERR = "✗"
|
|
46
|
+
GLYPH_DOT = "·"
|
|
47
|
+
GLYPH_ARROW = "→"
|
|
48
|
+
GLYPH_BULLET = "▸"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ─── Terminal capability detection ────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
def supports_truecolor() -> bool:
|
|
54
|
+
"""24-bit RGB ANSI. Same heuristic as _banner.py."""
|
|
55
|
+
if os.environ.get("NO_COLOR"):
|
|
56
|
+
return False
|
|
57
|
+
if os.environ.get("TERM", "").lower() == "dumb":
|
|
58
|
+
return False
|
|
59
|
+
# SIBYL_FORCE_COLOR=1 — explicit override for non-tty rendering
|
|
60
|
+
# (CI logs, doc captures, dev inspection inside the Claude harness).
|
|
61
|
+
if os.environ.get("SIBYL_FORCE_COLOR") == "1":
|
|
62
|
+
return True
|
|
63
|
+
if not sys.stdout.isatty():
|
|
64
|
+
return False
|
|
65
|
+
colorterm = os.environ.get("COLORTERM", "").lower()
|
|
66
|
+
if "truecolor" in colorterm or "24bit" in colorterm:
|
|
67
|
+
return True
|
|
68
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
69
|
+
if term_program in {"iterm.app", "wezterm", "ghostty", "vscode", "tabby"}:
|
|
70
|
+
return True
|
|
71
|
+
term = os.environ.get("TERM", "").lower()
|
|
72
|
+
if any(k in term for k in ("256color", "kitty", "alacritty", "xterm-direct")):
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def supports_color() -> bool:
|
|
78
|
+
"""Any color at all (3/4-bit fallback)."""
|
|
79
|
+
if os.environ.get("NO_COLOR"):
|
|
80
|
+
return False
|
|
81
|
+
if os.environ.get("TERM", "").lower() == "dumb":
|
|
82
|
+
return False
|
|
83
|
+
if os.environ.get("SIBYL_FORCE_COLOR") == "1":
|
|
84
|
+
return True
|
|
85
|
+
return sys.stdout.isatty()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_TC = supports_truecolor()
|
|
89
|
+
_C = supports_color()
|
|
90
|
+
RESET = "\033[0m" if _C else ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def rgb(r: int, g: int, b: int) -> str:
|
|
94
|
+
"""24-bit foreground escape (no-op if color disabled)."""
|
|
95
|
+
if not _TC:
|
|
96
|
+
return ""
|
|
97
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def rgb_bg(r: int, g: int, b: int) -> str:
|
|
101
|
+
if not _TC:
|
|
102
|
+
return ""
|
|
103
|
+
return f"\033[48;2;{r};{g};{b}m"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def color(text: str, c: tuple[int, int, int]) -> str:
|
|
107
|
+
if not _TC:
|
|
108
|
+
return text
|
|
109
|
+
return f"{rgb(*c)}{text}{RESET}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ─── Gradient · char-by-char RGB interpolation ────────────────────────
|
|
113
|
+
|
|
114
|
+
def _interp(a: int, b: int, t: float) -> int:
|
|
115
|
+
return round(a + (b - a) * t)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def gradient(text: str, *stops: tuple[int, int, int]) -> str:
|
|
119
|
+
"""Color a string with a gradient across N stops, one char at a time.
|
|
120
|
+
|
|
121
|
+
Plain-text fallback: returns the input unchanged when color is off.
|
|
122
|
+
Whitespace is preserved (uncolored to keep terminals consistent).
|
|
123
|
+
"""
|
|
124
|
+
if not _TC or len(stops) < 2 or not text:
|
|
125
|
+
return text
|
|
126
|
+
out = []
|
|
127
|
+
chars = list(text)
|
|
128
|
+
# Distribute char index across stop segments
|
|
129
|
+
n = max(1, len(chars) - 1)
|
|
130
|
+
segs = len(stops) - 1
|
|
131
|
+
for i, ch in enumerate(chars):
|
|
132
|
+
if ch == " ":
|
|
133
|
+
out.append(ch)
|
|
134
|
+
continue
|
|
135
|
+
seg_f = (i / n) * segs
|
|
136
|
+
seg_i = min(int(seg_f), segs - 1)
|
|
137
|
+
t = seg_f - seg_i
|
|
138
|
+
a = stops[seg_i]
|
|
139
|
+
b = stops[seg_i + 1]
|
|
140
|
+
r = _interp(a[0], b[0], t)
|
|
141
|
+
g = _interp(a[1], b[1], t)
|
|
142
|
+
bb = _interp(a[2], b[2], t)
|
|
143
|
+
out.append(f"\033[38;2;{r};{g};{bb}m{ch}")
|
|
144
|
+
return "".join(out) + RESET
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def gradient_gold(text: str) -> str:
|
|
148
|
+
"""Pale-gold → deep-ochre flow. The brand's headline gradient."""
|
|
149
|
+
return gradient(text, ACCENT_PALE, ACCENT_GOLD, ACCENT)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def gradient_jade(text: str) -> str:
|
|
153
|
+
"""Pulse → jade. Used for success states + live indicators."""
|
|
154
|
+
return gradient(text, PULSE, JADE)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─── Style primitives ─────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
def dim(s: str) -> str:
|
|
160
|
+
return color(s, INK_FAINT)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def muted(s: str) -> str:
|
|
164
|
+
return color(s, INK_MUTE)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def soft(s: str) -> str:
|
|
168
|
+
return color(s, INK_SOFT)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def ink(s: str) -> str:
|
|
172
|
+
return color(s, INK)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def ok(s: str) -> str:
|
|
176
|
+
return color(s, PULSE)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def warn(s: str) -> str:
|
|
180
|
+
return color(s, ACCENT_WARM)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def err(s: str) -> str:
|
|
184
|
+
return color(s, ERROR)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def accent(s: str) -> str:
|
|
188
|
+
return color(s, ACCENT)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def bold(s: str) -> str:
|
|
192
|
+
if not _C:
|
|
193
|
+
return s
|
|
194
|
+
return f"\033[1m{s}{RESET}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ─── Composite primitives ─────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
def eyebrow(label: str) -> str:
|
|
200
|
+
"""Uppercase letter-spaced ochre label. Editorial section marker."""
|
|
201
|
+
spaced = " ".join(label.upper())
|
|
202
|
+
return color(spaced, ACCENT)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def divider(width: int = 60, *, glyph: str = "─") -> str:
|
|
206
|
+
"""Creme-paper rule line."""
|
|
207
|
+
return color(glyph * width, RULE)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def section_header(name: str, *, subtitle: str | None = None, width: int = 60) -> str:
|
|
211
|
+
"""The standard subcommand opener.
|
|
212
|
+
|
|
213
|
+
─ <name> ────────────────────────────────────────
|
|
214
|
+
<subtitle, dim>
|
|
215
|
+
"""
|
|
216
|
+
name_part = f" {gradient_gold(name)} "
|
|
217
|
+
# Stripped-color length for visible width calc
|
|
218
|
+
visible_name_len = len(f" {name} ")
|
|
219
|
+
rule_left = "─"
|
|
220
|
+
rule_right = "─" * max(3, width - 1 - visible_name_len)
|
|
221
|
+
head = color(rule_left, RULE) + name_part + color(rule_right, RULE)
|
|
222
|
+
if subtitle:
|
|
223
|
+
return head + "\n" + dim(subtitle)
|
|
224
|
+
return head
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def chip(text: str, *, palette: str = "accent") -> str:
|
|
228
|
+
"""Compact inline label · [text]."""
|
|
229
|
+
palettes = {
|
|
230
|
+
"accent": ACCENT,
|
|
231
|
+
"jade": PULSE,
|
|
232
|
+
"warn": ACCENT_WARM,
|
|
233
|
+
"error": ERROR,
|
|
234
|
+
"mute": INK_MUTE,
|
|
235
|
+
}
|
|
236
|
+
c = palettes.get(palette, ACCENT)
|
|
237
|
+
return color(f"[{text}]", c)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def kv(label: str, value: str, *, label_width: int = 16, value_color: str = "ink") -> str:
|
|
241
|
+
"""One left-aligned label / value row.
|
|
242
|
+
|
|
243
|
+
Used across status / whoami / devices for the LOCAL / SERVER blocks.
|
|
244
|
+
"""
|
|
245
|
+
palettes = {
|
|
246
|
+
"ink": INK, "soft": INK_SOFT, "mute": INK_MUTE, "faint": INK_FAINT,
|
|
247
|
+
"accent": ACCENT, "ok": PULSE, "warn": ACCENT_WARM, "err": ERROR,
|
|
248
|
+
}
|
|
249
|
+
val_color = palettes.get(value_color, INK_SOFT)
|
|
250
|
+
return f" {color(label.ljust(label_width), INK_FAINT)} {color(value, val_color)}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def block_title(text: str) -> str:
|
|
254
|
+
"""Sub-section title within a command output. Like 'LOCAL' or 'SERVER'."""
|
|
255
|
+
return "\n" + eyebrow(text)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def success_line(text: str) -> str:
|
|
259
|
+
"""Single-line success marker with gradient + glyph."""
|
|
260
|
+
return f" {ok(GLYPH_OK)} {gradient_jade(text)}"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def warn_line(text: str) -> str:
|
|
264
|
+
return f" {warn(GLYPH_WARN)} {warn(text)}"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def err_line(text: str) -> str:
|
|
268
|
+
return f" {err(GLYPH_ERR)} {err(text)}"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def hr_caption(caption: str, *, width: int = 60) -> str:
|
|
272
|
+
"""Caption line under a divider — small, muted, centered."""
|
|
273
|
+
pad = max(0, (width - len(caption)) // 2)
|
|
274
|
+
return " " * pad + dim(caption)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def footer_credits(*, width: int = 60) -> str:
|
|
278
|
+
"""Bottom-of-output line. Used at end of long outputs."""
|
|
279
|
+
return color("─" * width, RULE) + "\n" + dim(" sibyl labs · memory you can hold in your hand")
|
|
@@ -59,6 +59,8 @@ UPGRADE_TIMEOUT_SEC = 15 * 60 # 15 minutes for upgrade — wallet ux can be sl
|
|
|
59
59
|
|
|
60
60
|
# ---- Color / output ----------------------------------------------------
|
|
61
61
|
|
|
62
|
+
from . import _aesthetic as a
|
|
63
|
+
|
|
62
64
|
_NO_COLOR = bool(os.environ.get("NO_COLOR")) or not sys.stdout.isatty()
|
|
63
65
|
|
|
64
66
|
|
|
@@ -106,7 +108,7 @@ class HttpError(Exception):
|
|
|
106
108
|
self.url = url
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def http_request(
|
|
111
|
+
def http_request( # noqa: D401
|
|
110
112
|
method: str,
|
|
111
113
|
path: str,
|
|
112
114
|
*,
|
|
@@ -215,10 +217,12 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
215
217
|
cred_path = Path(args.credentials).expanduser()
|
|
216
218
|
if cred_path.exists() and not args.force:
|
|
217
219
|
existing = read_credentials(cred_path) or {}
|
|
218
|
-
print(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
print(a.section_header("already activated", subtitle="use --force to re-activate"))
|
|
221
|
+
print()
|
|
222
|
+
print(a.kv("Account", short(existing.get("account_id"))))
|
|
223
|
+
print(a.kv("Tier", (existing.get("tier") or "free").upper(), value_color="accent"))
|
|
224
|
+
print(a.kv("Credentials", str(cred_path)))
|
|
225
|
+
print()
|
|
222
226
|
return 0
|
|
223
227
|
|
|
224
228
|
# SEC-1 mitigation (v0.1.2): the URL parameter is an opaque pairing
|
|
@@ -258,14 +262,16 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
258
262
|
print(yellow(f"Warning: session-init failed ({e.status}). Wallet path still works; email path may not."))
|
|
259
263
|
|
|
260
264
|
print()
|
|
261
|
-
print(
|
|
265
|
+
print(a.section_header("activation", subtitle="three paths · pick whichever fits your device"))
|
|
262
266
|
print()
|
|
263
|
-
print(
|
|
264
|
-
|
|
265
|
-
print(
|
|
266
|
-
|
|
267
|
+
print(a.kv("Session", short(session_id)))
|
|
268
|
+
formatted_code = pairing_code[:3] + " " + pairing_code[3:]
|
|
269
|
+
print(a.kv("Code", a.gradient_gold(formatted_code), value_color="accent")
|
|
270
|
+
+ " " + a.dim("(use this in the email panel)"))
|
|
271
|
+
print(a.kv("Opening", activate_url))
|
|
267
272
|
print()
|
|
268
|
-
print(dim("
|
|
273
|
+
print(a.dim(" desktop wallet · email + code · or send USDC from any mobile wallet"))
|
|
274
|
+
print(a.dim(" this terminal will pick up automatically when you bind."))
|
|
269
275
|
print()
|
|
270
276
|
|
|
271
277
|
try:
|
|
@@ -312,38 +318,46 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
312
318
|
creds["session_token"] = bearer
|
|
313
319
|
path = write_credentials_atomic(creds, cred_path)
|
|
314
320
|
print(f"\r{' ' * 80}\r", end="") # clear spinner line
|
|
315
|
-
print(green("✓ Activated."))
|
|
316
|
-
print_status("Account", short(creds.get("account_id")))
|
|
317
|
-
print_status("Tier", (creds.get("tier") or "free").upper())
|
|
318
|
-
print_status("Wallet", creds.get("wallet") or "—")
|
|
319
|
-
print_status("Email", creds.get("email") or "—")
|
|
320
|
-
print_status("Credentials", str(path))
|
|
321
321
|
print()
|
|
322
|
-
print(
|
|
323
|
-
print(
|
|
324
|
-
print(
|
|
325
|
-
print(
|
|
326
|
-
print(
|
|
322
|
+
print(a.success_line("Activated."))
|
|
323
|
+
print()
|
|
324
|
+
print(a.kv("Account", short(creds.get("account_id"))))
|
|
325
|
+
print(a.kv("Tier", (creds.get("tier") or "free").upper(), value_color="accent"))
|
|
326
|
+
print(a.kv("Wallet", creds.get("wallet") or "—"))
|
|
327
|
+
print(a.kv("Email", creds.get("email") or "—"))
|
|
328
|
+
print(a.kv("Credentials", str(path)))
|
|
329
|
+
print()
|
|
330
|
+
print(a.section_header("wire it into your agent"))
|
|
327
331
|
print()
|
|
328
|
-
print(dim("
|
|
329
|
-
print(dim("
|
|
332
|
+
print(a.dim(" hermes:"))
|
|
333
|
+
print(a.dim(" sibyl-memory-hermes install-plugin"))
|
|
334
|
+
print(a.dim(" # then edit ~/.hermes/config.yaml:"))
|
|
335
|
+
print(a.dim(" # memory:"))
|
|
336
|
+
print(a.dim(" # provider: sibyl"))
|
|
337
|
+
print()
|
|
338
|
+
print(a.dim(" claude code / codex / cursor / continue (MCP):"))
|
|
339
|
+
print(a.dim(" pip install sibyl-memory-mcp"))
|
|
340
|
+
print()
|
|
341
|
+
print(a.dim(" python orchestration (langchain / llamaindex / custom):"))
|
|
342
|
+
print(a.dim(" from sibyl_memory_hermes import SibylMemoryProvider"))
|
|
343
|
+
print(a.dim(" provider = SibylMemoryProvider()"))
|
|
330
344
|
print()
|
|
331
|
-
print(dim("Or call the SDK directly from any Python orchestration:"))
|
|
332
|
-
print(dim(" from sibyl_memory_hermes import SibylMemoryProvider"))
|
|
333
|
-
print(dim(" provider = SibylMemoryProvider()"))
|
|
334
345
|
return 0
|
|
335
346
|
|
|
336
347
|
# Spinner tick
|
|
337
348
|
spin_i = (spin_i + 1) % len(spinner)
|
|
338
349
|
remaining = int(deadline - time.time())
|
|
339
|
-
|
|
350
|
+
spin_glyph = a.color(spinner[spin_i], a.PULSE)
|
|
351
|
+
status = f"\r {spin_glyph} {a.dim('watching the network for your bind')} … {a.dim(f'{remaining // 60}:{remaining % 60:02d} left')}"
|
|
340
352
|
if status != last_status:
|
|
341
353
|
sys.stdout.write(status)
|
|
342
354
|
sys.stdout.flush()
|
|
343
355
|
last_status = status
|
|
344
356
|
time.sleep(POLL_INTERVAL_SEC)
|
|
345
357
|
|
|
346
|
-
print(
|
|
358
|
+
print()
|
|
359
|
+
print(a.err_line("Activation timed out."))
|
|
360
|
+
print(a.dim(" Re-run `sibyl init` to try again."))
|
|
347
361
|
return 1
|
|
348
362
|
|
|
349
363
|
|
|
@@ -353,7 +367,8 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
|
353
367
|
"""Upgrade flow. Read existing creds → open upgrade page → poll /access until tier flips."""
|
|
354
368
|
creds = read_credentials(Path(args.credentials).expanduser())
|
|
355
369
|
if not creds:
|
|
356
|
-
print(
|
|
370
|
+
print(a.err_line("Not activated."))
|
|
371
|
+
print(a.dim(" Run `sibyl init` first."))
|
|
357
372
|
return 1
|
|
358
373
|
|
|
359
374
|
account_id = creds.get("account_id")
|
|
@@ -361,21 +376,22 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
|
361
376
|
current_tier = (creds.get("tier") or "free").lower()
|
|
362
377
|
|
|
363
378
|
if not account_id or not session_token:
|
|
364
|
-
print(
|
|
379
|
+
print(a.err_line("credentials.json is missing account_id or session_token."))
|
|
380
|
+
print(a.dim(" Re-run `sibyl init`."))
|
|
365
381
|
return 1
|
|
366
382
|
|
|
367
383
|
upgrade_url = f"{UPGRADE_BASE}?session={session_token}"
|
|
368
384
|
|
|
369
385
|
print()
|
|
370
|
-
print(
|
|
386
|
+
print(a.section_header("upgrade", subtitle="lift the 2 MB free-tier cap"))
|
|
371
387
|
print()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
print(a.kv("Account", short(account_id)))
|
|
389
|
+
print(a.kv("Current tier", current_tier.upper(), value_color="accent"))
|
|
390
|
+
print(a.kv("Opening", upgrade_url))
|
|
375
391
|
print()
|
|
376
|
-
print(dim("
|
|
377
|
-
print(dim("
|
|
378
|
-
print(dim("
|
|
392
|
+
print(a.dim(" two paths in the browser:"))
|
|
393
|
+
print(a.dim(" 1. stake $SIBYL on Base (free unlimited if you qualify)"))
|
|
394
|
+
print(a.dim(" 2. subscribe in USDC (monthly / quarterly / annual)"))
|
|
379
395
|
print()
|
|
380
396
|
|
|
381
397
|
try:
|
|
@@ -416,78 +432,90 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
|
416
432
|
invalidate_tier_cache()
|
|
417
433
|
|
|
418
434
|
print(f"\r{' ' * 80}\r", end="")
|
|
419
|
-
print(
|
|
420
|
-
|
|
435
|
+
print()
|
|
436
|
+
print(a.success_line(f"Upgraded to {new_tier.upper()} via {source}."))
|
|
437
|
+
print()
|
|
438
|
+
print(a.kv("Source", source))
|
|
421
439
|
if resp.get("expires_at"):
|
|
422
|
-
|
|
440
|
+
print(a.kv("Expires", resp["expires_at"]))
|
|
423
441
|
if resp.get("cap_bytes") is None:
|
|
424
|
-
|
|
442
|
+
print(a.kv("Storage cap", "unlimited", value_color="ok"))
|
|
425
443
|
else:
|
|
426
|
-
|
|
444
|
+
print(a.kv("Storage cap", f"{resp['cap_bytes']:,} bytes"))
|
|
427
445
|
if resp.get("staker"):
|
|
428
446
|
s = resp["staker"]
|
|
429
|
-
|
|
430
|
-
|
|
447
|
+
print(a.kv("Wallet", s.get("wallet", "—")))
|
|
448
|
+
print(a.kv("$SIBYL held", str(s.get("total_sibyl", "—"))))
|
|
431
449
|
print()
|
|
432
|
-
print(dim("
|
|
450
|
+
print(a.dim(" local tier cache cleared. your next write will sync the new tier."))
|
|
433
451
|
return 0
|
|
434
452
|
|
|
435
453
|
spin_i = (spin_i + 1) % len(spinner)
|
|
436
454
|
remaining = int(deadline - time.time())
|
|
437
|
-
|
|
455
|
+
spin_glyph = a.color(spinner[spin_i], a.PULSE)
|
|
456
|
+
tier_glyph = a.color(current_tier.upper(), a.ACCENT)
|
|
457
|
+
status = f"\r {spin_glyph} {a.dim('waiting for browser upgrade')} · current: {tier_glyph} {a.dim(f'{remaining // 60}:{remaining % 60:02d} left')}"
|
|
438
458
|
if status != last_status:
|
|
439
459
|
sys.stdout.write(status)
|
|
440
460
|
sys.stdout.flush()
|
|
441
461
|
last_status = status
|
|
442
462
|
time.sleep(POLL_INTERVAL_SEC)
|
|
443
463
|
|
|
444
|
-
print(
|
|
464
|
+
print()
|
|
465
|
+
print(a.err_line("Upgrade timed out. Tier unchanged."))
|
|
466
|
+
print(a.dim(" Re-run `sibyl upgrade` to retry."))
|
|
445
467
|
return 1
|
|
446
468
|
|
|
447
469
|
|
|
448
470
|
# ---- `sibyl status` ----------------------------------------------------
|
|
449
471
|
|
|
450
472
|
def cmd_status(args: argparse.Namespace) -> int:
|
|
451
|
-
"""Show local + server-side state without modifying anything.
|
|
473
|
+
"""Show local + server-side state without modifying anything.
|
|
474
|
+
|
|
475
|
+
LIGHT treatment: utilitarian dashboard. No banner, no section header,
|
|
476
|
+
no chrome. Eyebrow labels + kv rows + ↓ status drift surfaces. Same
|
|
477
|
+
convention as `git status`, `ls -la`, `btop` panel bodies."""
|
|
452
478
|
cred_path = Path(args.credentials).expanduser()
|
|
453
479
|
creds = read_credentials(cred_path)
|
|
454
480
|
|
|
455
|
-
print()
|
|
456
|
-
print(bold("Sibyl Memory Plugin · status"))
|
|
457
481
|
print()
|
|
458
482
|
|
|
459
483
|
if not creds:
|
|
460
|
-
print(
|
|
484
|
+
print(a.warn_line("Not activated."))
|
|
485
|
+
print(a.dim(" Run `sibyl init`."))
|
|
461
486
|
return 0
|
|
462
487
|
|
|
463
488
|
# Local view
|
|
464
|
-
print(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
print(a.eyebrow("local"))
|
|
490
|
+
print(a.kv("Credentials", str(cred_path)))
|
|
491
|
+
print(a.kv("Account", short(creds.get("account_id"))))
|
|
492
|
+
print(a.kv("Tier", (creds.get("tier") or "free").upper(), value_color="accent"))
|
|
493
|
+
print(a.kv("Wallet", creds.get("wallet") or "—"))
|
|
494
|
+
print(a.kv("Email", creds.get("email") or "—"))
|
|
495
|
+
print(a.kv("Issued", creds.get("issued_at") or "—"))
|
|
471
496
|
|
|
472
497
|
db_path = Path(args.db).expanduser()
|
|
473
498
|
if db_path.exists():
|
|
474
499
|
size = db_path.stat().st_size
|
|
475
|
-
|
|
476
|
-
|
|
500
|
+
pct = size / 2_097_152 * 100
|
|
501
|
+
size_label = f"{size:,} bytes ({size / (1024 * 1024):.2f} MB · {pct:.1f}% of free cap)"
|
|
502
|
+
size_color = "warn" if pct > 80 else "soft"
|
|
503
|
+
print(a.kv("DB path", str(db_path)))
|
|
504
|
+
print(a.kv("DB size", size_label, value_color=size_color))
|
|
477
505
|
else:
|
|
478
|
-
|
|
506
|
+
print(a.kv("DB path", f"{db_path} (not created)"))
|
|
479
507
|
|
|
480
508
|
tier_cache = Path(args.tier_cache).expanduser()
|
|
481
509
|
if tier_cache.exists():
|
|
482
510
|
cache = json.loads(tier_cache.read_text(encoding="utf-8"))
|
|
483
|
-
|
|
511
|
+
print(a.kv("Tier cache", f"{cache.get('tier','?')} (checked {cache.get('checked_at','?')[:19]})"))
|
|
484
512
|
else:
|
|
485
|
-
|
|
513
|
+
print(a.kv("Tier cache", "—"))
|
|
486
514
|
|
|
487
515
|
# Server view (only if account_id + session_token are present)
|
|
488
516
|
if creds.get("account_id") and creds.get("session_token"):
|
|
489
517
|
print()
|
|
490
|
-
print(
|
|
518
|
+
print(a.eyebrow("server"))
|
|
491
519
|
try:
|
|
492
520
|
resp = http_request(
|
|
493
521
|
"POST",
|
|
@@ -495,25 +523,26 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
495
523
|
body={"account_id": creds["account_id"], "session_token": creds["session_token"]},
|
|
496
524
|
timeout=10.0,
|
|
497
525
|
)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
526
|
+
print(a.kv("Tier", (resp.get("tier") or "free").upper(), value_color="accent"))
|
|
527
|
+
print(a.kv("Source", resp.get("source") or "—"))
|
|
528
|
+
print(a.kv("Cap bytes", "unlimited" if resp.get("cap_bytes") is None else f"{resp['cap_bytes']:,}"))
|
|
501
529
|
if resp.get("expires_at"):
|
|
502
|
-
|
|
530
|
+
print(a.kv("Expires", resp["expires_at"]))
|
|
503
531
|
if resp.get("staker"):
|
|
504
532
|
s = resp["staker"]
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
533
|
+
print(a.kv("$SIBYL held", str(s.get("total_sibyl", "—"))))
|
|
534
|
+
print(a.kv("Threshold", str(s.get("threshold_sibyl", "—"))))
|
|
535
|
+
print(a.kv("Qualified", "yes" if s.get("qualified") else "no",
|
|
536
|
+
value_color="ok" if s.get("qualified") else "soft"))
|
|
508
537
|
# Detect server/local drift
|
|
509
538
|
srv_tier = (resp.get("tier") or "free").lower()
|
|
510
539
|
loc_tier = (creds.get("tier") or "free").lower()
|
|
511
540
|
if srv_tier != loc_tier:
|
|
512
541
|
print()
|
|
513
|
-
print(
|
|
514
|
-
print(dim(" Run `sibyl upgrade` to refresh, or
|
|
542
|
+
print(a.warn_line(f"Local tier ({loc_tier}) differs from server tier ({srv_tier})."))
|
|
543
|
+
print(a.dim(" Run `sibyl upgrade` to refresh, or `sibyl init --force` to re-activate."))
|
|
515
544
|
except HttpError as e:
|
|
516
|
-
|
|
545
|
+
print(a.kv("Tier", f"server error: {e.status}", value_color="err"))
|
|
517
546
|
|
|
518
547
|
print()
|
|
519
548
|
return 0
|
|
@@ -550,6 +579,151 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
550
579
|
return cmd_status(args)
|
|
551
580
|
|
|
552
581
|
|
|
582
|
+
# ---- `sibyl whoami` ----------------------------------------------------
|
|
583
|
+
|
|
584
|
+
def _mask_email(e: str | None) -> str:
|
|
585
|
+
if not e or "@" not in e:
|
|
586
|
+
return "—"
|
|
587
|
+
user, _, domain = e.partition("@")
|
|
588
|
+
if "." not in domain:
|
|
589
|
+
return f"{user[0]}***@{domain[0]}***"
|
|
590
|
+
name, _, tld = domain.rpartition(".")
|
|
591
|
+
return f"{user[0]}***@{name[0]}***.{tld}"
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _mask_wallet(w: str | None) -> str:
|
|
595
|
+
if not w or not w.startswith("0x") or len(w) < 12:
|
|
596
|
+
return w or "—"
|
|
597
|
+
return f"{w[:6]}…{w[-4:]}"
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def cmd_whoami(args: argparse.Namespace) -> int:
|
|
601
|
+
"""One-line account summary. Shows account_id + tier + linked email/wallet + this device.
|
|
602
|
+
|
|
603
|
+
LIGHT treatment: 4-line glance. No banner, no section header. Same shape
|
|
604
|
+
as `whoami` on unix, `gh auth status`, `aws sts get-caller-identity`."""
|
|
605
|
+
creds = read_credentials(Path(args.credentials).expanduser())
|
|
606
|
+
if not creds:
|
|
607
|
+
print(a.warn_line("Not activated."))
|
|
608
|
+
print(a.dim(" Run `sibyl init`."))
|
|
609
|
+
return 1
|
|
610
|
+
|
|
611
|
+
full = bool(getattr(args, "full", False))
|
|
612
|
+
acct = creds.get("account_id") or ""
|
|
613
|
+
tier = (creds.get("tier") or "free").upper()
|
|
614
|
+
email = creds.get("email") if full else _mask_email(creds.get("email"))
|
|
615
|
+
wallet = creds.get("wallet") if full else _mask_wallet(creds.get("wallet"))
|
|
616
|
+
|
|
617
|
+
print()
|
|
618
|
+
print(f" {a.color('account', a.INK_FAINT)} {a.bold(short(acct))} {a.dim(a.GLYPH_DOT)} {a.gradient_gold(tier)}")
|
|
619
|
+
print(f" {a.color('wallet ', a.INK_FAINT)} {a.color(wallet or '—', a.INK)}")
|
|
620
|
+
print(f" {a.color('email ', a.INK_FAINT)} {a.color(email or '—', a.INK)}")
|
|
621
|
+
os_label = _detect_os_family() or "unknown"
|
|
622
|
+
device_line = f"sibyl-memory-cli/{_client_version()} {os_label}"
|
|
623
|
+
print(f" {a.color('device ', a.INK_FAINT)} {a.dim(device_line)}")
|
|
624
|
+
print()
|
|
625
|
+
return 0
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# ---- `sibyl devices` ---------------------------------------------------
|
|
629
|
+
|
|
630
|
+
def cmd_devices(args: argparse.Namespace) -> int:
|
|
631
|
+
"""List active bearer tokens (devices) for the account. Optional: revoke by index."""
|
|
632
|
+
creds = read_credentials(Path(args.credentials).expanduser())
|
|
633
|
+
if not creds:
|
|
634
|
+
print(a.err_line("Not activated."))
|
|
635
|
+
print(a.dim(" Run `sibyl init`."))
|
|
636
|
+
return 1
|
|
637
|
+
account_id = creds.get("account_id")
|
|
638
|
+
session_token = creds.get("session_token")
|
|
639
|
+
if not account_id or not session_token:
|
|
640
|
+
print(a.err_line("credentials.json missing account_id or session_token."))
|
|
641
|
+
print(a.dim(" Run `sibyl init`."))
|
|
642
|
+
return 1
|
|
643
|
+
|
|
644
|
+
sub = getattr(args, "sub", None)
|
|
645
|
+
|
|
646
|
+
# `sibyl devices revoke <index>` path
|
|
647
|
+
if sub == "revoke":
|
|
648
|
+
idx = getattr(args, "index", None)
|
|
649
|
+
if idx is None:
|
|
650
|
+
print(red("usage: sibyl devices revoke <index>"))
|
|
651
|
+
return 1
|
|
652
|
+
# List first to map index → bearer_id
|
|
653
|
+
try:
|
|
654
|
+
resp = http_request(
|
|
655
|
+
"GET",
|
|
656
|
+
f"/api/plugin/devices?account_id={urllib.parse.quote(account_id)}",
|
|
657
|
+
headers={"Authorization": f"Bearer {session_token}"},
|
|
658
|
+
timeout=10.0,
|
|
659
|
+
)
|
|
660
|
+
except HttpError as e:
|
|
661
|
+
print(red(f"server error: {e.status} {e.body}"))
|
|
662
|
+
return 2
|
|
663
|
+
devices = resp.get("devices", [])
|
|
664
|
+
try:
|
|
665
|
+
target = devices[idx]
|
|
666
|
+
except (IndexError, TypeError):
|
|
667
|
+
print(red(f"no device at index {idx}. Run `sibyl devices` to see indexes."))
|
|
668
|
+
return 1
|
|
669
|
+
if target.get("is_this_device"):
|
|
670
|
+
print(red("refusing to revoke your own device — that would lock you out. Run `sibyl logout` instead, then `sibyl init` on a fresh activation."))
|
|
671
|
+
return 1
|
|
672
|
+
try:
|
|
673
|
+
revoke_resp = http_request(
|
|
674
|
+
"POST",
|
|
675
|
+
"/api/plugin/devices",
|
|
676
|
+
body={"bearer_id": target["bearer_id"]},
|
|
677
|
+
headers={"Authorization": f"Bearer {session_token}"},
|
|
678
|
+
timeout=10.0,
|
|
679
|
+
)
|
|
680
|
+
except HttpError as e:
|
|
681
|
+
print(red(f"revoke failed: {e.status} {e.body}"))
|
|
682
|
+
return 2
|
|
683
|
+
print(green(f"✓ Revoked device {target.get('device_label') or target['bearer_id']}"))
|
|
684
|
+
return 0 if revoke_resp.get("revoked") else 1
|
|
685
|
+
|
|
686
|
+
# Default: list devices
|
|
687
|
+
try:
|
|
688
|
+
resp = http_request(
|
|
689
|
+
"GET",
|
|
690
|
+
f"/api/plugin/devices?account_id={urllib.parse.quote(account_id)}",
|
|
691
|
+
headers={"Authorization": f"Bearer {session_token}"},
|
|
692
|
+
timeout=10.0,
|
|
693
|
+
)
|
|
694
|
+
except HttpError as e:
|
|
695
|
+
if e.status == 401:
|
|
696
|
+
print(a.err_line("Session expired."))
|
|
697
|
+
print(a.dim(" Re-run `sibyl init`."))
|
|
698
|
+
else:
|
|
699
|
+
print(a.err_line(f"server error: {e.status} {e.body}"))
|
|
700
|
+
return 2
|
|
701
|
+
|
|
702
|
+
devices = resp.get("devices", [])
|
|
703
|
+
# LIGHT treatment: table-like dashboard. Eyebrow line with count + the rows. No banner.
|
|
704
|
+
print()
|
|
705
|
+
print(f" {a.eyebrow('devices')} {a.dim(f'· {len(devices)} active')}")
|
|
706
|
+
print()
|
|
707
|
+
if not devices:
|
|
708
|
+
print(a.dim(" no active devices"))
|
|
709
|
+
print()
|
|
710
|
+
return 0
|
|
711
|
+
|
|
712
|
+
for i, d in enumerate(devices):
|
|
713
|
+
is_this = d.get("is_this_device")
|
|
714
|
+
marker = a.ok("▶") if is_this else " "
|
|
715
|
+
label = d.get("device_label") or "(unlabeled)"
|
|
716
|
+
installed = d.get("install_method") or "—"
|
|
717
|
+
last_seen = d.get("last_seen_at", "")[:19].replace("T", " ")
|
|
718
|
+
idx_chip = a.chip(str(i), palette="jade" if is_this else "mute")
|
|
719
|
+
label_color = a.gradient_gold(label) if is_this else a.color(label, a.INK)
|
|
720
|
+
meta = f"{a.dim(installed)} {a.dim(a.GLYPH_DOT)} {a.dim('last seen ' + last_seen)}"
|
|
721
|
+
note = a.color("(this device)", a.PULSE) if is_this else a.dim(f"revoke: sibyl devices revoke {i}")
|
|
722
|
+
print(f" {marker} {idx_chip} {label_color} {meta} {note}")
|
|
723
|
+
print()
|
|
724
|
+
return 0
|
|
725
|
+
|
|
726
|
+
|
|
553
727
|
# ---- `sibyl logout` ----------------------------------------------------
|
|
554
728
|
|
|
555
729
|
def cmd_logout(args: argparse.Namespace) -> int:
|
|
@@ -565,16 +739,17 @@ def cmd_logout(args: argparse.Namespace) -> int:
|
|
|
565
739
|
tier_cache.unlink()
|
|
566
740
|
deleted.append(str(tier_cache))
|
|
567
741
|
|
|
742
|
+
# LIGHT treatment: quick confirmation. No banner, no section header.
|
|
568
743
|
print()
|
|
569
744
|
if not deleted:
|
|
570
|
-
print(
|
|
745
|
+
print(a.warn_line("Nothing to remove."))
|
|
746
|
+
print(a.dim(" Already logged out."))
|
|
571
747
|
else:
|
|
572
|
-
print(
|
|
748
|
+
print(a.success_line("Logged out."))
|
|
573
749
|
for path in deleted:
|
|
574
|
-
print(f" {dim('removed')} {path}")
|
|
750
|
+
print(f" {a.dim('removed')} {a.color(path, a.INK)}")
|
|
575
751
|
print()
|
|
576
|
-
print(dim("
|
|
577
|
-
print(dim("Run `sibyl init` to activate a fresh account."))
|
|
752
|
+
print(a.dim(" memory.db untouched. run `sibyl init` to activate a fresh account."))
|
|
578
753
|
print()
|
|
579
754
|
return 0
|
|
580
755
|
|
|
@@ -586,12 +761,28 @@ def cmd_health(args: argparse.Namespace) -> int:
|
|
|
586
761
|
try:
|
|
587
762
|
from sibyl_memory_hermes import SibylMemoryProvider
|
|
588
763
|
except ImportError:
|
|
589
|
-
print(
|
|
764
|
+
print(a.err_line("sibyl-memory-hermes not installed."))
|
|
765
|
+
print(a.dim(" pip install sibyl-memory-hermes"))
|
|
590
766
|
return 1
|
|
767
|
+
|
|
768
|
+
# LIGHT treatment: verdict + details. No banner, no section header.
|
|
769
|
+
# Pattern: `pg_isready` / `redis-cli ping` / `gh auth status`.
|
|
770
|
+
print()
|
|
591
771
|
provider = SibylMemoryProvider(db_path=args.db)
|
|
592
772
|
h = provider.health()
|
|
593
|
-
|
|
594
|
-
|
|
773
|
+
ok_state = bool(h.get("ok"))
|
|
774
|
+
if ok_state:
|
|
775
|
+
print(a.success_line("All green."))
|
|
776
|
+
else:
|
|
777
|
+
print(a.err_line("Health check reports issues."))
|
|
778
|
+
print()
|
|
779
|
+
for k, v in h.items():
|
|
780
|
+
if k == "ok":
|
|
781
|
+
continue
|
|
782
|
+
val = str(v)
|
|
783
|
+
print(a.kv(k, val, value_color="ok" if v is True else ("soft" if v else "warn")))
|
|
784
|
+
print()
|
|
785
|
+
return 0 if ok_state else 1
|
|
595
786
|
|
|
596
787
|
|
|
597
788
|
# ---- Dispatch ----------------------------------------------------------
|
|
@@ -620,6 +811,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
620
811
|
p_st = sub.add_parser("status", help="Show local + server tier / DB stats")
|
|
621
812
|
p_st.set_defaults(func=cmd_status)
|
|
622
813
|
|
|
814
|
+
p_who = sub.add_parser("whoami", help="One-line account summary (masked by default)")
|
|
815
|
+
p_who.add_argument("--full", action="store_true", help="Show full email + wallet (no masking)")
|
|
816
|
+
p_who.set_defaults(func=cmd_whoami)
|
|
817
|
+
|
|
818
|
+
p_dev = sub.add_parser("devices", help="List devices (active bearer tokens) for the account")
|
|
819
|
+
dev_sub = p_dev.add_subparsers(dest="sub")
|
|
820
|
+
p_rev = dev_sub.add_parser("revoke", help="Revoke a device by index (run `sibyl devices` for indexes)")
|
|
821
|
+
p_rev.add_argument("index", type=int, help="Index from `sibyl devices` output")
|
|
822
|
+
p_dev.set_defaults(func=cmd_devices)
|
|
823
|
+
p_rev.set_defaults(func=cmd_devices)
|
|
824
|
+
|
|
623
825
|
p_dash = sub.add_parser("dashboard", help="Open the account dashboard (delegates to status until account.sibyllabs.org ships)")
|
|
624
826
|
p_dash.set_defaults(func=cmd_dashboard)
|
|
625
827
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.1
|
|
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.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://sibyllabs.org/plugin
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.1.4 → sibyl_memory_cli-0.3.1}/src/sibyl_memory_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|