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,288 @@
1
+ #!/usr/bin/env python3
2
+ """zeno_attention - the single source of truth for the cognitive-attention model.
3
+
4
+ This is the ONE attention formula. The HUD statusline (the writer) imports it to
5
+ compute the score it persists into cognition_samples, and any other Python reader
6
+ (exporter, dashboard SSR helpers) presents those persisted rows. Because the
7
+ writer and the bar both derive their number from this module - and the bar reads
8
+ the row this module produced - the in-terminal HUD and the tailnet dashboard can
9
+ never diverge. See docs/COGNITION_UNIFIED_PLAN.md (Phase 2: shared signal).
10
+
11
+ The attention signal is a novel cognitive read derived from the session
12
+ transcript: how much effort/precision goes into prompts (effort), the
13
+ deliberation rhythm between turns (deliberation), and whether engagement is
14
+ rising or fading (trend). It nudges "keep going" when sharp and "rest" when
15
+ fading.
16
+
17
+ Design: stdlib-only, Python 3.9+ safe, never raises. The model is a documented,
18
+ tunable v1 heuristic; env vars ZENO_HUD_* override the weights/window so the HUD
19
+ and any consumer that imports this module read identical knobs.
20
+
21
+ The TS mirror of this formula lives in the dashboard; keep the two in lockstep
22
+ (weights, thresholds, label bands) when tuning - that is the whole contract.
23
+ """
24
+
25
+ import json
26
+ import math
27
+ import os
28
+ import re
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # tunable model knobs (env-overridable; shared by every importer of this module)
32
+ # ---------------------------------------------------------------------------
33
+ # attention weights (effort / deliberation / trend), normalized internally
34
+ W_EFFORT = float(os.environ.get("ZENO_HUD_W_EFFORT", "0.55"))
35
+ W_DELIB = float(os.environ.get("ZENO_HUD_W_DELIB", "0.30"))
36
+ W_TREND = float(os.environ.get("ZENO_HUD_W_TREND", "0.15"))
37
+ WINDOW = int(os.environ.get("ZENO_HUD_WINDOW", "6")) # recent prompts considered
38
+ TAIL_BYTES = int(os.environ.get("ZENO_HUD_TAIL_BYTES", "1048576")) # transcript tail to read
39
+
40
+ # label bands (score 0..100 -> label/nudge): the single classifier the bar and
41
+ # the dashboard both honor. Colors are ANSI codes for the HUD; the dashboard
42
+ # maps the same bands to its theme.
43
+ GREEN, YELLOW, RED, CYAN = "92", "93", "91", "96"
44
+
45
+ _TAG_RE = re.compile(r"<(/?)(command-[\w-]+|local-command-[\w-]+|system-reminder)[^>]*>")
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # prompt cleaning + per-prompt effort
50
+ # ---------------------------------------------------------------------------
51
+ def clean_prompt_text(content):
52
+ """Extract human-authored prompt text, dropping injected wrappers/tool-results."""
53
+ if isinstance(content, str):
54
+ text = content
55
+ elif isinstance(content, list):
56
+ parts = []
57
+ for b in content:
58
+ if not isinstance(b, dict):
59
+ continue
60
+ if b.get("type") == "tool_result": # this is a tool return, not a prompt
61
+ return ""
62
+ if b.get("type") == "text" and isinstance(b.get("text"), str):
63
+ parts.append(b["text"])
64
+ text = "\n".join(parts)
65
+ else:
66
+ return ""
67
+ text = _TAG_RE.sub("", text) # strip <command-*>/<system-reminder> tags
68
+ # drop lines that are pure injected blocks the user didn't type
69
+ keep = [
70
+ ln for ln in text.splitlines() if not ln.lstrip().startswith(("<", "Caveat:", "[SYSTEM"))
71
+ ]
72
+ return "\n".join(keep).strip()
73
+
74
+
75
+ def prompt_effort(text):
76
+ """0..1 effort score for one prompt: longer + structured + code = more effort."""
77
+ n = len(text)
78
+ base = 1.0 - math.exp(-n / 180.0) # ~0 tiny, ~0.8 @300 chars, ->1 long paragraph
79
+ bonus = 0.0
80
+ if "```" in text or text.count("\n") >= 2:
81
+ bonus += 0.15
82
+ if n <= 6 and text.lower() in ("y", "yes", "go", "ok", "continue", "do it", "ship it", "next"):
83
+ base *= 0.25 # one-word "let the AI lead" prompts
84
+ return max(0.0, min(1.0, base + bonus))
85
+
86
+
87
+ def delib_score(gap_s):
88
+ """0..1 health of a between-turn gap: rapid-fire coasting low, thoughtful mid-high."""
89
+ if gap_s is None:
90
+ return 0.5
91
+ if gap_s < 4:
92
+ return 0.30 # rapid-fire / coasting
93
+ if gap_s <= 180:
94
+ return 1.0 # active thinking
95
+ if gap_s <= 900:
96
+ return 0.6 # slower, still engaged
97
+ return None # >15min: away, exclude from mean
98
+
99
+
100
+ def parse_ts(s):
101
+ if not isinstance(s, str):
102
+ return None
103
+ try:
104
+ s = s.replace("Z", "+0000").replace("-00:00", "+0000")
105
+ # tolerate fractional seconds + tz; parse epoch via time.strptime on the date part
106
+ m = re.match(r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})", s)
107
+ if not m:
108
+ return None
109
+ import calendar
110
+
111
+ return calendar.timegm(tuple(int(x) for x in m.groups()) + (0, 0, 0))
112
+ except Exception:
113
+ return None
114
+
115
+
116
+ def parse_transcript(path):
117
+ """Read the transcript tail; return (prompts, asst_turns).
118
+ prompts: list of (ts_epoch, effort 0..1). asst_turns: list of (ts_epoch, out_tokens, tool_uses).
119
+ """
120
+ prompts, asst = [], []
121
+ if not path or not os.path.exists(path):
122
+ return prompts, asst
123
+ try:
124
+ size = os.path.getsize(path)
125
+ with open(path, "rb") as f:
126
+ if size > TAIL_BYTES:
127
+ f.seek(size - TAIL_BYTES)
128
+ f.readline() # discard partial first line
129
+ data = f.read().decode("utf-8", "replace")
130
+ except Exception:
131
+ return prompts, asst
132
+ for line in data.splitlines():
133
+ line = line.strip()
134
+ if not line:
135
+ continue
136
+ try:
137
+ o = json.loads(line)
138
+ except Exception:
139
+ continue
140
+ t = o.get("type")
141
+ if t not in ("user", "assistant"):
142
+ continue
143
+ ts = parse_ts(o.get("timestamp"))
144
+ msg = o.get("message") or {}
145
+ if t == "user":
146
+ if o.get("isMeta") or o.get("isSidechain"):
147
+ continue
148
+ text = clean_prompt_text(msg.get("content"))
149
+ if not text:
150
+ continue
151
+ prompts.append((ts, prompt_effort(text)))
152
+ else:
153
+ usage = msg.get("usage") or {}
154
+ out = usage.get("output_tokens") or 0
155
+ content = msg.get("content")
156
+ tools = (
157
+ sum(1 for b in content if isinstance(b, dict) and b.get("type") == "tool_use")
158
+ if isinstance(content, list)
159
+ else 0
160
+ )
161
+ asst.append((ts, out, tools))
162
+ return prompts, asst
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # the formula
167
+ # ---------------------------------------------------------------------------
168
+ def classify(score, trend, long_session):
169
+ """Map a score (+ trend, session length) to (label, nudge, glyph, color).
170
+
171
+ The single classifier the HUD bar and the dashboard both honor: the bands
172
+ here are the contract. Returns plain data so a reader presenting a persisted
173
+ row reproduces the exact same label/nudge/glyph the writer would have shown.
174
+ """
175
+ glyph = "▲" if trend > 0.08 else ("▼" if trend < -0.08 else "─")
176
+ if score >= 70:
177
+ label, nudge, col = "sharp", "keep going", GREEN
178
+ elif score >= 45:
179
+ label, nudge, col = "steady", "in rhythm", CYAN
180
+ elif score >= 25:
181
+ label, nudge, col = "easing", ("wrap soon" if long_session else "ride it out"), YELLOW
182
+ else:
183
+ label, nudge, col = "fading", "rest", RED
184
+ if long_session and trend < -0.2 and score < 70:
185
+ nudge = "rest soon"
186
+ col = RED if score < 45 else YELLOW
187
+ return label, nudge, glyph, col
188
+
189
+
190
+ def attention(prompts, asst):
191
+ """Compute the attention signal from parsed transcript data.
192
+
193
+ Returns dict(ok, score 0..100, trend -1..1, effort, deliberation, label,
194
+ nudge, glyph, color) or {"ok": False} when there is too little data. This is
195
+ the canonical computation: the HUD writer persists score/effort/deliberation/
196
+ trend from here into cognition_samples, and present_sample() rebuilds the
197
+ label/nudge/glyph from the same classifier - so a row read back is identical
198
+ to a row freshly computed.
199
+ """
200
+ if len(prompts) < 2:
201
+ return {"ok": False}
202
+ recent = prompts[-WINDOW:]
203
+ efforts = [e for _, e in recent]
204
+ effort_now = sum(efforts) / len(efforts)
205
+
206
+ # deliberation: gap from each prompt back to the prior assistant turn (or prior prompt)
207
+ gaps = []
208
+ asst_ts = [a[0] for a in asst if a[0] is not None]
209
+ for ts, _ in recent:
210
+ if ts is None:
211
+ continue
212
+ prior = [x for x in asst_ts if x <= ts]
213
+ if prior:
214
+ gaps.append(ts - max(prior))
215
+ delib_scores = [s for s in (delib_score(g) for g in gaps) if s is not None]
216
+ delib = sum(delib_scores) / len(delib_scores) if delib_scores else 0.5
217
+
218
+ # trend: recent half vs earlier half of the window
219
+ if len(efforts) >= 4:
220
+ half = len(efforts) // 2
221
+ trend = max(
222
+ -1.0,
223
+ min(1.0, (sum(efforts[half:]) / (len(efforts) - half)) - (sum(efforts[:half]) / half)),
224
+ )
225
+ else:
226
+ trend = 0.0
227
+
228
+ wsum = W_EFFORT + W_DELIB + W_TREND
229
+ raw = (W_EFFORT * effort_now + W_DELIB * delib + W_TREND * (0.5 + 0.5 * trend)) / wsum
230
+ score = int(round(max(0.0, min(1.0, raw)) * 100))
231
+
232
+ # session length (minutes) for fatigue framing
233
+ ts_all = [t for t, _ in prompts if t is not None]
234
+ session_min = (max(ts_all) - min(ts_all)) / 60.0 if len(ts_all) >= 2 else 0.0
235
+ long_session = session_min >= 120
236
+
237
+ label, nudge, glyph, col = classify(score, trend, long_session)
238
+ return {
239
+ "ok": True,
240
+ "score": score,
241
+ "trend": trend,
242
+ "effort": effort_now,
243
+ "deliberation": delib,
244
+ "label": label,
245
+ "nudge": nudge,
246
+ "glyph": glyph,
247
+ "color": col,
248
+ }
249
+
250
+
251
+ def present_sample(row):
252
+ """Rebuild the renderable attention dict from a persisted cognition_samples row.
253
+
254
+ `row` is a mapping with attention_score / attention_trend (+ optional
255
+ attention_effort, attention_deliberation, ts pairs for session length).
256
+ Re-derives label/nudge/glyph via the same `classify` the live path uses, so
257
+ the bar shown from a stored row equals the bar shown from a live compute.
258
+ Returns {"ok": False} when the row has no usable score.
259
+ """
260
+ if not row:
261
+ return {"ok": False}
262
+ score = row.get("attention_score")
263
+ if score is None:
264
+ return {"ok": False}
265
+ try:
266
+ score = int(round(float(score)))
267
+ except (TypeError, ValueError):
268
+ return {"ok": False}
269
+ trend = row.get("attention_trend")
270
+ trend = float(trend) if isinstance(trend, (int, float)) else 0.0
271
+ long_session = bool(row.get("long_session"))
272
+ label, nudge, glyph, col = classify(score, trend, long_session)
273
+ out = {
274
+ "ok": True,
275
+ "score": score,
276
+ "trend": trend,
277
+ "label": label,
278
+ "nudge": nudge,
279
+ "glyph": glyph,
280
+ "color": col,
281
+ }
282
+ eff = row.get("attention_effort")
283
+ if isinstance(eff, (int, float)):
284
+ out["effort"] = float(eff)
285
+ delib = row.get("attention_deliberation")
286
+ if isinstance(delib, (int, float)):
287
+ out["deliberation"] = float(delib)
288
+ return out