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.
- zeno_adapters/__init__.py +17 -0
- zeno_adapters/_common.py +38 -0
- zeno_adapters/anthropic.py +68 -0
- zeno_adapters/claude_code.py +101 -0
- zeno_adapters/crewai.py +92 -0
- zeno_adapters/langgraph.py +49 -0
- zeno_adapters/openai.py +108 -0
- zeno_cli/__init__.py +1 -0
- zeno_cli/_hooks/cc_bridge.py +1016 -0
- zeno_cli/doctor.py +535 -0
- zeno_cli/hook_install.py +269 -0
- zeno_cli/hud/__init__.py +1 -0
- zeno_cli/hud/hud_install.py +652 -0
- zeno_cli/hud/zeno_attention.py +288 -0
- zeno_cli/hud/zeno_cognition.py +457 -0
- zeno_cli/hud/zeno_hud.py +496 -0
- zeno_cli/interview_invites.py +342 -0
- zeno_cli/login.py +241 -0
- zeno_cli/main.py +2534 -0
- zeno_cli/onboard.py +206 -0
- zeno_cli/outreach.py +456 -0
- zeno_cli/version.py +67 -0
- zeno_cli-0.3.4.dist-info/METADATA +161 -0
- zeno_cli-0.3.4.dist-info/RECORD +69 -0
- zeno_cli-0.3.4.dist-info/WHEEL +4 -0
- zeno_cli-0.3.4.dist-info/entry_points.txt +4 -0
- zeno_core/__init__.py +67 -0
- zeno_core/analytics.py +193 -0
- zeno_core/rtlx_s.py +460 -0
- zeno_core/streak.py +178 -0
- zeno_core/tlx_s.py +192 -0
- zeno_sdk/__init__.py +6 -0
- zeno_sdk/_generated/__init__.py +6 -0
- zeno_sdk/_generated/client.py +819 -0
- zeno_sdk/_migrations/alembic/env.py +33 -0
- zeno_sdk/_migrations/alembic/script.py.mako +18 -0
- zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
- zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
- zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
- zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
- zeno_sdk/_migrations/alembic.ini +35 -0
- zeno_sdk/_runtime.py +12 -0
- zeno_sdk/adapters/__init__.py +15 -0
- zeno_sdk/adapters/anthropic.py +5 -0
- zeno_sdk/adapters/claude_code.py +5 -0
- zeno_sdk/adapters/crewai.py +5 -0
- zeno_sdk/adapters/langgraph.py +5 -0
- zeno_sdk/adapters/openai.py +5 -0
- zeno_sdk/auth.py +25 -0
- zeno_sdk/client.py +87 -0
- zeno_sdk/config.py +61 -0
- zeno_sdk/daemon.py +72 -0
- zeno_sdk/privacy.py +46 -0
- zeno_sdk/session.py +179 -0
- zeno_sdk/storage.py +487 -0
- zeno_sdk/types/__init__.py +121 -0
- zeno_session_intel/__init__.py +19 -0
- zeno_session_intel/analytics.py +588 -0
- zeno_session_intel/compression.py +123 -0
- zeno_session_intel/ingest.py +376 -0
- zeno_session_intel/model.py +129 -0
- zeno_session_intel/parsers/__init__.py +31 -0
- zeno_session_intel/parsers/claude_code.py +169 -0
- zeno_session_intel/parsers/codex.py +265 -0
- zeno_session_intel/parsers/cursor.py +198 -0
- zeno_session_intel/prices.py +281 -0
- zeno_session_intel/schema.py +277 -0
- zeno_session_intel/signals.py +319 -0
- 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
|