sibyl-memory-cli 0.1.3__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.1.3
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
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  Requires-Dist: sibyl-memory-client>=0.4.0
22
22
  Requires-Dist: sibyl-memory-hermes>=0.3.2
23
+ Requires-Dist: pyyaml>=6.0
23
24
  Provides-Extra: dev
24
25
  Requires-Dist: pytest>=7.0; extra == "dev"
25
26
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sibyl-memory-cli"
7
- version = "0.1.3"
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"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  dependencies = [
26
26
  "sibyl-memory-client>=0.4.0",
27
27
  "sibyl-memory-hermes>=0.3.2",
28
+ "pyyaml>=6.0",
28
29
  ]
29
30
 
30
31
  [project.optional-dependencies]
@@ -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")