zeno-cli 0.3.4__py3-none-any.whl

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.
Files changed (69) hide show
  1. zeno_adapters/__init__.py +17 -0
  2. zeno_adapters/_common.py +38 -0
  3. zeno_adapters/anthropic.py +68 -0
  4. zeno_adapters/claude_code.py +101 -0
  5. zeno_adapters/crewai.py +92 -0
  6. zeno_adapters/langgraph.py +49 -0
  7. zeno_adapters/openai.py +108 -0
  8. zeno_cli/__init__.py +1 -0
  9. zeno_cli/_hooks/cc_bridge.py +1016 -0
  10. zeno_cli/doctor.py +535 -0
  11. zeno_cli/hook_install.py +269 -0
  12. zeno_cli/hud/__init__.py +1 -0
  13. zeno_cli/hud/hud_install.py +652 -0
  14. zeno_cli/hud/zeno_attention.py +288 -0
  15. zeno_cli/hud/zeno_cognition.py +457 -0
  16. zeno_cli/hud/zeno_hud.py +496 -0
  17. zeno_cli/interview_invites.py +342 -0
  18. zeno_cli/login.py +241 -0
  19. zeno_cli/main.py +2534 -0
  20. zeno_cli/onboard.py +206 -0
  21. zeno_cli/outreach.py +456 -0
  22. zeno_cli/version.py +67 -0
  23. zeno_cli-0.3.4.dist-info/METADATA +161 -0
  24. zeno_cli-0.3.4.dist-info/RECORD +69 -0
  25. zeno_cli-0.3.4.dist-info/WHEEL +4 -0
  26. zeno_cli-0.3.4.dist-info/entry_points.txt +4 -0
  27. zeno_core/__init__.py +67 -0
  28. zeno_core/analytics.py +193 -0
  29. zeno_core/rtlx_s.py +460 -0
  30. zeno_core/streak.py +178 -0
  31. zeno_core/tlx_s.py +192 -0
  32. zeno_sdk/__init__.py +6 -0
  33. zeno_sdk/_generated/__init__.py +6 -0
  34. zeno_sdk/_generated/client.py +819 -0
  35. zeno_sdk/_migrations/alembic/env.py +33 -0
  36. zeno_sdk/_migrations/alembic/script.py.mako +18 -0
  37. zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
  38. zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
  39. zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
  40. zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
  41. zeno_sdk/_migrations/alembic.ini +35 -0
  42. zeno_sdk/_runtime.py +12 -0
  43. zeno_sdk/adapters/__init__.py +15 -0
  44. zeno_sdk/adapters/anthropic.py +5 -0
  45. zeno_sdk/adapters/claude_code.py +5 -0
  46. zeno_sdk/adapters/crewai.py +5 -0
  47. zeno_sdk/adapters/langgraph.py +5 -0
  48. zeno_sdk/adapters/openai.py +5 -0
  49. zeno_sdk/auth.py +25 -0
  50. zeno_sdk/client.py +87 -0
  51. zeno_sdk/config.py +61 -0
  52. zeno_sdk/daemon.py +72 -0
  53. zeno_sdk/privacy.py +46 -0
  54. zeno_sdk/session.py +179 -0
  55. zeno_sdk/storage.py +487 -0
  56. zeno_sdk/types/__init__.py +121 -0
  57. zeno_session_intel/__init__.py +19 -0
  58. zeno_session_intel/analytics.py +588 -0
  59. zeno_session_intel/compression.py +123 -0
  60. zeno_session_intel/ingest.py +376 -0
  61. zeno_session_intel/model.py +129 -0
  62. zeno_session_intel/parsers/__init__.py +31 -0
  63. zeno_session_intel/parsers/claude_code.py +169 -0
  64. zeno_session_intel/parsers/codex.py +265 -0
  65. zeno_session_intel/parsers/cursor.py +198 -0
  66. zeno_session_intel/prices.py +281 -0
  67. zeno_session_intel/schema.py +277 -0
  68. zeno_session_intel/signals.py +319 -0
  69. zeno_session_intel/taxonomy.py +71 -0
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env python3
2
+ """zeno-hud - a Claude Code statusline that shows, every render (~300ms):
3
+
4
+ zeno · Opus 4.8 · zeno
5
+ ctx ███████░░░ 68% 5h ██░░░░░░░░ 22% wk ████░░░░░░ 41%
6
+ att ███████▇░░ 74 ▲ sharp · keep going eff ████████ 60 · drv ██████░░ 45
7
+
8
+ The first three meters (context %, 5h usage %, weekly usage %) come straight from
9
+ the JSON Claude Code pipes on stdin - no API call. The bottom line is the cognition
10
+ read - three bars sharing one model (zeno_cognition):
11
+
12
+ att the composite attention score, with its label/nudge ("sharp · keep going")
13
+ and, when notable, the most-deviant OFF-bar driver as a tag (e.g. ↑fatigue).
14
+ eff how hard you are driving this turn - the effort driver.
15
+ drv how hands-on you are vs delegating - the inverse of the autonomy (AI-led)
16
+ driver, i.e. your share of the wheel.
17
+
18
+ Capped at three bars on purpose. The session count + RTLX-S SCED progress moved to
19
+ the tailnet dashboard - the bar stays a pure, glanceable cognitive read.
20
+
21
+ Design: stdlib-only, Python 3.9+ safe, never raises (a broken statusline shows
22
+ nothing), targets <60ms. The attention model is a documented, tunable v1 heuristic
23
+ (see README.md); env vars ZENO_HUD_* override the weights/widths.
24
+
25
+ Pairs with the zeno-cc-bridge hook (the WRITE/capture surface); this is the READ
26
+ surface. Wire via ~/.claude/settings.json statusLine -> see README.md.
27
+ """
28
+
29
+ import json
30
+ import os
31
+ import re
32
+ import sys
33
+
34
+ # zeno_attention is the single source of truth for the attention formula: the
35
+ # cc-bridge hook persists the score and the bar reads it back, so HUD and dashboard
36
+ # never diverge (see docs/COGNITION_UNIFIED_PLAN.md Phase 2). zeno_cognition is the v2
37
+ # per-turn model the cc-bridge hook writes and the HUD reads back through
38
+ # COG.present_sample (falling back to the v1 transcript compute when no row exists).
39
+ # Both are siblings in this package (zeno_cli.hud), so the imports are plain relative.
40
+ from . import zeno_attention as A
41
+ from . import zeno_cognition as COG
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # config (env-overridable)
45
+ # ---------------------------------------------------------------------------
46
+ NO_COLOR = bool(os.environ.get("NO_COLOR"))
47
+ BAR_W = int(os.environ.get("ZENO_HUD_BAR_WIDTH", "10"))
48
+ ATT_BAR_W = int(os.environ.get("ZENO_HUD_ATT_WIDTH", "10"))
49
+ COG_BAR_W = int(os.environ.get("ZENO_HUD_COG_WIDTH", "8")) # the eff/drv sub-bars
50
+ ZENO_DB = os.environ.get("ZENO_DB_PATH") or os.path.join(
51
+ os.environ.get("ZENO_HOME", os.path.join(os.path.expanduser("~"), ".zeno")), "zeno.db"
52
+ )
53
+ # attention model knobs live in the shared module; re-exported so existing
54
+ # importers/tests that read H.WINDOW / H.TAIL_BYTES keep working unchanged.
55
+ W_EFFORT, W_DELIB, W_TREND = A.W_EFFORT, A.W_DELIB, A.W_TREND
56
+ WINDOW = A.WINDOW
57
+ TAIL_BYTES = A.TAIL_BYTES
58
+
59
+ # ANSI codes (GREEN/YELLOW/RED/CYAN match zeno_attention's color bands)
60
+ GREEN, YELLOW, RED, CYAN, DIM, BOLD = "92", "93", "91", "96", "90", "1"
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # rendering primitives
65
+ # ---------------------------------------------------------------------------
66
+ def c(s, code):
67
+ """Wrap s in an ANSI color unless NO_COLOR."""
68
+ if NO_COLOR or code is None:
69
+ return s
70
+ return f"\x1b[{code}m{s}\x1b[0m"
71
+
72
+
73
+ def bar(frac, width=BAR_W):
74
+ """A unicode block bar with eighth-cell partials. Returns exactly `width` chars."""
75
+ frac = 0.0 if frac is None else max(0.0, min(1.0, frac))
76
+ eighths = int(round(frac * width * 8))
77
+ full, rem = divmod(eighths, 8)
78
+ full = min(full, width)
79
+ out = "█" * full
80
+ if full < width:
81
+ out += " ▏▎▍▌▋▊▉"[rem] if rem else "░"
82
+ out += "░" * (width - full - 1)
83
+ return out
84
+
85
+
86
+ def pct_color(pct, lo=70, hi=85):
87
+ """Context/usage coloring: green under lo, yellow lo..hi, red above hi."""
88
+ if pct is None:
89
+ return DIM
90
+ if pct < lo:
91
+ return GREEN
92
+ if pct < hi:
93
+ return YELLOW
94
+ return RED
95
+
96
+
97
+ def meter(label, pct, width=BAR_W, lo=70, hi=85):
98
+ if pct is None:
99
+ return "{} {} {}".format(c(label, DIM), c("░" * width, DIM), c("--", DIM))
100
+ col = pct_color(pct, lo, hi)
101
+ return "{} {} {}".format(c(label, DIM), c(bar(pct / 100.0, width), col), f"{pct:>3.0f}%")
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # stdin (the native, accurate data)
106
+ # ---------------------------------------------------------------------------
107
+ def read_stdin():
108
+ try:
109
+ raw = sys.stdin.read()
110
+ except Exception:
111
+ return {}
112
+ raw = (raw or "").strip()
113
+ if not raw:
114
+ return {}
115
+ try:
116
+ return json.loads(raw)
117
+ except Exception:
118
+ return {}
119
+
120
+
121
+ def _dig(d, *path):
122
+ cur = d
123
+ for k in path:
124
+ if not isinstance(cur, dict):
125
+ return None
126
+ cur = cur.get(k)
127
+ return cur
128
+
129
+
130
+ def context_pct(stdin):
131
+ p = _dig(stdin, "context_window", "used_percentage")
132
+ if isinstance(p, (int, float)):
133
+ return float(p)
134
+ # fall back to current_usage / size
135
+ used = _dig(stdin, "context_window", "current_usage", "input_tokens")
136
+ size = _dig(stdin, "context_window", "context_window_size")
137
+ if isinstance(used, (int, float)) and isinstance(size, (int, float)) and size:
138
+ return 100.0 * used / size
139
+ return None
140
+
141
+
142
+ def rate_pct(stdin, window):
143
+ p = _dig(stdin, "rate_limits", window, "used_percentage")
144
+ return float(p) if isinstance(p, (int, float)) else None
145
+
146
+
147
+ def session_id(stdin):
148
+ """Map the Claude Code session id to the same zeno session UUID the
149
+ cc-bridge uses (uuid5 over 'cc:<id>'), so HUD samples and bridge
150
+ sessions/runs join on session_id. Returns None if no session id."""
151
+ cc = stdin.get("session_id")
152
+ if not isinstance(cc, str) or not cc:
153
+ return None
154
+ try:
155
+ import uuid
156
+
157
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, "cc:" + cc))
158
+ except Exception:
159
+ return None
160
+
161
+
162
+ def effort_level(stdin):
163
+ e = stdin.get("effort")
164
+ if isinstance(e, dict):
165
+ return e.get("level")
166
+ if isinstance(e, str):
167
+ return e
168
+ return None
169
+
170
+
171
+ def model_name(stdin):
172
+ name = _dig(stdin, "model", "display_name") or ""
173
+ name = re.sub(r"\s*\([^)]*context[^)]*\)", "", name).strip() # strip "(1M context)"
174
+ return name or "claude"
175
+
176
+
177
+ def project_name(stdin):
178
+ d = _dig(stdin, "workspace", "current_dir") or stdin.get("cwd") or ""
179
+ return os.path.basename(d.rstrip("/")) if d else ""
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # cognitive-attention engine - the formula lives in zeno_attention (single
184
+ # source of truth). These thin aliases preserve the public names the HUD and
185
+ # its tests call so the writer + bar + dashboard all share one model.
186
+ # ---------------------------------------------------------------------------
187
+ _clean_prompt_text = A.clean_prompt_text
188
+ _prompt_effort = A.prompt_effort
189
+ _delib_score = A.delib_score
190
+ _parse_ts = A.parse_ts
191
+ parse_transcript = A.parse_transcript
192
+ attention = A.attention
193
+
194
+
195
+ def attention_meter(stdin, a=None, width=None):
196
+ width = ATT_BAR_W if width is None else width
197
+ if a is None:
198
+ prompts, asst = parse_transcript(stdin.get("transcript_path"))
199
+ a = attention(prompts, asst)
200
+ if not a.get("ok"):
201
+ return "{} {} {}".format(c("att", DIM), c("░" * width, DIM), c("--", DIM))
202
+ # v2: append the most-deviant driver ("one big thing") as a small arrow tag.
203
+ # effort + autonomy already have their own bars on this line (eff / drv), so the
204
+ # tag only surfaces an OFF-bar driver (verification / fatigue / flow) - the thing
205
+ # you would otherwise miss. Absent on v1 rows / live fallback.
206
+ tag = ""
207
+ top = a.get("top_driver")
208
+ drivers = a.get("drivers")
209
+ if top and top not in ("effort", "autonomy") and isinstance(drivers, dict) and top in drivers:
210
+ v = drivers[top]
211
+ arrow = "↑" if v >= 55 else ("↓" if v <= 45 else "·")
212
+ tag = " " + c(arrow + top, DIM)
213
+ return "{} {} {} {} {}{}".format(
214
+ c("att", DIM),
215
+ c(bar(a["score"] / 100.0, width), a["color"]),
216
+ "{:>2d}".format(a["score"]),
217
+ c(a["glyph"] + " " + a["label"], a["color"]),
218
+ c("· " + a["nudge"], DIM),
219
+ tag,
220
+ )
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # cognition sub-bars (eff / drv) - the two component drivers shown beside the
225
+ # composite attention bar. They read the same per-turn drivers the cc-bridge hook
226
+ # writes; no DB query of their own.
227
+ # ---------------------------------------------------------------------------
228
+ def _driver_pct(a, name):
229
+ """0..100 for a named v2 driver from the presented attention dict, else None.
230
+
231
+ The cc-bridge hook writes the five standardized drivers and the HUD reads them
232
+ back. Absent on a v1 row / fresh session, where the caller falls back."""
233
+ drivers = a.get("drivers")
234
+ if isinstance(drivers, dict):
235
+ v = drivers.get(name)
236
+ if isinstance(v, (int, float)):
237
+ return float(v)
238
+ return None
239
+
240
+
241
+ def effort_pct(a):
242
+ """The 'eff' bar value (0..100): how hard the human is driving this turn.
243
+
244
+ Prefers the v2 effort driver (standardized to your own baseline; 50 == your
245
+ median). Falls back to the v1 raw 0..1 effort scaled to 0..100 when no driver
246
+ row exists yet (no hook / fresh session)."""
247
+ p = _driver_pct(a, "effort")
248
+ if p is not None:
249
+ return p
250
+ v1 = a.get("effort")
251
+ return float(v1) * 100.0 if isinstance(v1, (int, float)) else None
252
+
253
+
254
+ def drive_pct(a):
255
+ """The 'drv' bar value (0..100): how hands-on you are vs delegating - your share
256
+ of the wheel. It is the inverse of the autonomy driver (the AI-led ratio: tool
257
+ activity + thin prompts + long autonomous runs). v2 only - None on a v1 row."""
258
+ auto = _driver_pct(a, "autonomy")
259
+ return (100.0 - auto) if auto is not None else None
260
+
261
+
262
+ def cog_meter(label, pct, width=COG_BAR_W):
263
+ """A neutral cognition sub-bar: label + bar + 0..100 value.
264
+
265
+ Unlike the usage meters these carry NO good/bad coloring - high effort or low
266
+ drive are not 'bad' (a clean autonomous run is healthy delegation), so the fill
267
+ is a flat accent (CYAN) and the read stays descriptive, not a target. Renders a
268
+ dim '--' when the value is absent (v1 row / fresh session)."""
269
+ if pct is None:
270
+ return "{} {} {}".format(c(label, DIM), c("░" * width, DIM), c("--", DIM))
271
+ return "{} {} {}".format(c(label, DIM), c(bar(pct / 100.0, width), CYAN), f"{pct:>2.0f}")
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # cognition_samples reader (the READ half). The WRITE half lives in the
276
+ # zeno-cc-bridge hook (the event-time writer of the rich v2 rows); the bar is
277
+ # read-only, so the statusline render never writes the store.
278
+ # ---------------------------------------------------------------------------
279
+ def latest_cognition_sample(sid):
280
+ """Read the most recent persisted cognition_samples row for this session.
281
+
282
+ Returns a dict of column->value (the attention_* + token + ts fields) or
283
+ None when there is no DB, no table, no row for the session, or any error.
284
+ Read-only and best-effort: it must never slow or break the render. This is
285
+ the READ half that lets the bar show the exact row the dashboard reads, so
286
+ the two surfaces can never disagree."""
287
+ if sid is None:
288
+ return None
289
+ try:
290
+ import sqlite3
291
+ except Exception:
292
+ return None
293
+ if not os.path.exists(ZENO_DB):
294
+ return None
295
+ try:
296
+ con = sqlite3.connect(f"file:{ZENO_DB}?mode=ro", uri=True, timeout=0.3)
297
+ try:
298
+ con.row_factory = sqlite3.Row
299
+ # SELECT * so we pick up the v2 driver columns (attention_autonomy/
300
+ # verification/fatigue/flow) when present, and still work on a v1 DB
301
+ # that lacks them (present_sample treats absent drivers as missing).
302
+ row = con.execute(
303
+ """
304
+ SELECT * FROM cognition_samples
305
+ WHERE session_id = ?
306
+ ORDER BY ts DESC
307
+ LIMIT 1
308
+ """,
309
+ (sid,),
310
+ ).fetchone()
311
+ finally:
312
+ con.close()
313
+ return dict(row) if row is not None else None
314
+ except Exception:
315
+ return None
316
+
317
+
318
+ def attention_for_render(stdin, live_att):
319
+ """The attention dict the bar should show: prefer the persisted sample so the
320
+ HUD matches the dashboard exactly, fall back to the live compute when no row
321
+ exists yet (fresh session, capture disabled, or read failure).
322
+
323
+ `live_att` is the freshly computed attention dict (the fallback). When a stored row
324
+ carries a score, we re-present it through the shared classifier so the rendered bar
325
+ equals the row a dashboard would draw - identical by construction."""
326
+ sid = session_id(stdin)
327
+ row = latest_cognition_sample(sid)
328
+ if row is not None:
329
+ # carry the live long-session framing so the nudge band matches; the row
330
+ # itself does not persist session length.
331
+ if isinstance(live_att, dict) and live_att.get("nudge") in ("wrap soon", "rest soon"):
332
+ row = dict(row)
333
+ row["long_session"] = True
334
+ # Pick the presenter by what the row actually carries. A v2 row (any of the
335
+ # four v2 driver columns present) goes through COG.present_sample (drivers +
336
+ # off-bar tag). A v1-only row (no v2 drivers) goes through A.present_sample,
337
+ # which scales the legacy 0..1 attention_effort correctly - so eff is right,
338
+ # not a 0..100 misread of a 0..1 value. Each falls back to the other if the
339
+ # chosen presenter cannot render the row, then to the live compute.
340
+ has_v2 = any(
341
+ isinstance(row.get("attention_" + d), (int, float))
342
+ for d in ("autonomy", "verification", "fatigue", "flow")
343
+ )
344
+ presented = COG.present_sample(row) if has_v2 else A.present_sample(row)
345
+ if not presented.get("ok"):
346
+ presented = A.present_sample(row) if has_v2 else COG.present_sample(row)
347
+ if presented.get("ok"):
348
+ return presented
349
+ return live_att
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # the cognition bar (the one differentiated line)
354
+ # ---------------------------------------------------------------------------
355
+ def _bar_widths():
356
+ """Pick (att_bar_width, cog_bar_width) for the current terminal.
357
+
358
+ The ZENO_HUD_*_WIDTH env knobs are the wide-terminal default (att 10, eff/drv 8),
359
+ and that default is also exactly the COLUMNS-unset behavior - so the bar's output
360
+ is byte-for-byte the historical `line3` whenever COLUMNS is unset or comfortably
361
+ wide (the golden regression + determinism hold). When COLUMNS IS set and narrow,
362
+ the three bars shrink together to a floor of 3 cells so the line stays single-line
363
+ on a narrow pane; on a wide pane the bars never grow past the knob default and
364
+ never pad, so there is no overflow or padding garbage. Width is presentation only -
365
+ the att/eff/drv numbers and labels (the cognition meaning) are unchanged."""
366
+ att_w, cog_w = ATT_BAR_W, COG_BAR_W
367
+ cols = os.environ.get("COLUMNS")
368
+ if not cols:
369
+ return att_w, cog_w
370
+ try:
371
+ cols = int(cols)
372
+ except (TypeError, ValueError):
373
+ return att_w, cog_w
374
+ # at >= 90 cols the default bars + their text fit comfortably; below that, scale
375
+ # the bars down linearly to a floor so the fill shrinks instead of forcing a wrap.
376
+ if cols >= 90:
377
+ return att_w, cog_w
378
+ scale = max(0.0, (cols - 10)) / 80.0 # 1.0 at 90 cols, ~0.375 at 40, 0 at <=10
379
+ return max(3, int(round(att_w * scale))), max(3, int(round(cog_w * scale)))
380
+
381
+
382
+ def render_bar(stdin):
383
+ """Render the single differentiated cognition line - the former HUD `line3`.
384
+
385
+ Output (one line): the composite attention bar (with its label/nudge and, when
386
+ notable, the most-deviant OFF-bar driver tag), then the two component sub-bars -
387
+ effort (how hard you drive) and drive (how hands-on vs delegating). Capped at
388
+ three bars by design; the session + SCED counters live on the dashboard now.
389
+
390
+ This is the renderer shared by the whole-HUD `zeno-hud` (its third line) and the
391
+ standalone `zeno-hud-bar` add-on, so the two can never diverge. It is READ-ONLY:
392
+ it reads the latest persisted cognition_samples row (written by the zeno-cc-bridge
393
+ capture hook) and re-presents it, falling back to a live transcript compute when no
394
+ row exists yet. The statusline render never writes the store.
395
+ """
396
+ prompts, asst = parse_transcript(stdin.get("transcript_path"))
397
+ live_att = attention(prompts, asst)
398
+ # READ-only: render the bar from the latest persisted sample (falling back to the
399
+ # live compute) so the in-terminal HUD shows the exact number the dashboard reads
400
+ # from the same row - zero divergence by construction. The cc-bridge hook is the
401
+ # writer; the bar never writes (decision revised 2026-06-26: the bar is pure-read).
402
+ att = attention_for_render(stdin, live_att)
403
+ att_w, cog_w = _bar_widths()
404
+ return "{} {} · {}".format(
405
+ attention_meter(stdin, att, att_w),
406
+ cog_meter("eff", effort_pct(att), cog_w),
407
+ cog_meter("drv", drive_pct(att), cog_w),
408
+ )
409
+
410
+
411
+ # ---------------------------------------------------------------------------
412
+ # render
413
+ # ---------------------------------------------------------------------------
414
+ def render(stdin):
415
+ line1_parts = [c("zeno", BOLD)]
416
+ mn = model_name(stdin)
417
+ if mn:
418
+ line1_parts.append(c(mn, DIM))
419
+ pn = project_name(stdin)
420
+ if pn:
421
+ line1_parts.append(c(pn, DIM))
422
+ eff = effort_level(stdin)
423
+ if eff:
424
+ line1_parts.append(c("⚡" + eff, DIM))
425
+ line1 = c(" · ", DIM).join(line1_parts)
426
+
427
+ line2 = " ".join(
428
+ [
429
+ meter("ctx", context_pct(stdin), BAR_W, 70, 85),
430
+ meter("5h", rate_pct(stdin, "five_hour"), BAR_W, 60, 85),
431
+ meter("wk", rate_pct(stdin, "seven_day"), BAR_W, 60, 85),
432
+ ]
433
+ )
434
+
435
+ # the cognition line is the shared bar renderer, so the whole HUD's third line
436
+ # and the standalone zeno-hud-bar are byte-for-byte identical (the golden
437
+ # regression). render_bar also owns the throttled cognition_samples write.
438
+ line3 = render_bar(stdin)
439
+
440
+ return "\n".join([line1, line2, line3])
441
+
442
+
443
+ def _read_stdin_object():
444
+ """Read stdin for the bar: return (ok, dict).
445
+
446
+ ok is False for the three crash-safe-empty cases - no/empty stdin, malformed
447
+ JSON, or valid JSON whose top level is not an object - so bar_main prints an
448
+ empty line. ok is True for any JSON object (even `{}`), where render_bar
449
+ degrades the missing fields to `--` rather than emitting nothing.
450
+ """
451
+ try:
452
+ raw = sys.stdin.read()
453
+ except Exception:
454
+ return False, {}
455
+ raw = (raw or "").strip()
456
+ if not raw:
457
+ return False, {}
458
+ try:
459
+ data = json.loads(raw)
460
+ except Exception:
461
+ return False, {}
462
+ if not isinstance(data, dict):
463
+ return False, {}
464
+ return True, data
465
+
466
+
467
+ def main():
468
+ try:
469
+ sys.stdout.write(render(read_stdin()))
470
+ except Exception:
471
+ # a statusline must never crash; degrade to the brand mark
472
+ sys.stdout.write(c("zeno", BOLD))
473
+ sys.stdout.write("\n")
474
+
475
+
476
+ def bar_main():
477
+ """Entry point for the `zeno-hud-bar` console_script and `zeno hud bar`.
478
+
479
+ Crash-safe contract (stricter than main): on ANY error - no/empty stdin,
480
+ malformed JSON, or an internal failure - print an empty line and exit 0. The
481
+ bar is an APPENDAGE stacked UNDER the host HUD (claude-hud / ccstatusline), so
482
+ its failure must never pollute the host's output. main() prints the `zeno`
483
+ brand mark because it owns the whole statusline; the bar owns only one row, so
484
+ its fallback is an empty row, not a brand mark.
485
+ """
486
+ try:
487
+ ok, stdin = _read_stdin_object()
488
+ out = render_bar(stdin) if ok else ""
489
+ except Exception:
490
+ out = ""
491
+ sys.stdout.write(out)
492
+ sys.stdout.write("\n")
493
+
494
+
495
+ if __name__ == "__main__":
496
+ main()