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
zeno_cli/hook_install.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""`zeno hook install`: read-merge-write of Claude Code's ``~/.claude/settings.json``.
|
|
2
|
+
|
|
3
|
+
zeno OWNS this merge. Claude Code does not reliably merge hooks across config
|
|
4
|
+
scopes (open bugs both ways: hooks merge-not-replace and top-level keys
|
|
5
|
+
replace-not-merge), so we read the file, deep-merge our entries, and write it
|
|
6
|
+
back, with three safety rails from the 2026-06-25 distribution research:
|
|
7
|
+
|
|
8
|
+
1. A timestamped ``.zeno-bak.<stamp>`` backup before any write.
|
|
9
|
+
2. A JSON parse error ABORTS (never swallowed): returning ``{}`` on a malformed
|
|
10
|
+
but present settings.json would truncate the user's real config on write.
|
|
11
|
+
3. A ``zeno-managed-hook`` marker on every command we add, as a trailing shell
|
|
12
|
+
comment, so ``zeno hook uninstall`` removes exactly our entries and a
|
|
13
|
+
re-install is idempotent (no duplicate capture).
|
|
14
|
+
|
|
15
|
+
See ``docs/ADOPT.md`` and the ``[[zeno-internal-distribution]]`` memory.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# The cc-bridge hook dispatches internally on ``hook_event_name`` from its stdin
|
|
28
|
+
# payload, so the SAME command is registered for each of these five events.
|
|
29
|
+
HOOK_EVENTS: tuple[str, ...] = (
|
|
30
|
+
"SessionStart",
|
|
31
|
+
"UserPromptSubmit",
|
|
32
|
+
"Stop",
|
|
33
|
+
"Notification",
|
|
34
|
+
"SessionEnd",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ZENO_MARKER = "zeno-managed-hook"
|
|
38
|
+
DEFAULT_HOOK_COMMAND = "zeno hook run"
|
|
39
|
+
|
|
40
|
+
# A handler/statusLine is treated as zeno-owned if its command contains any of
|
|
41
|
+
# these. Includes the legacy dotfiles registrations so a first `zeno hook
|
|
42
|
+
# install` MIGRATES them in place instead of duplicating capture.
|
|
43
|
+
_ZENO_SIGNATURES: tuple[str, ...] = (
|
|
44
|
+
ZENO_MARKER,
|
|
45
|
+
"zeno hook run",
|
|
46
|
+
"zeno-cc-bridge",
|
|
47
|
+
"zeno_cc_bridge",
|
|
48
|
+
"zeno-hud",
|
|
49
|
+
"zeno_hud",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HookInstallError(Exception):
|
|
54
|
+
"""Raised when settings.json is unsafe to edit (malformed / wrong shape)."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_settings_path() -> Path:
|
|
58
|
+
"""``~/.claude/settings.json``, honoring CLAUDE_CONFIG_DIR if the user set it."""
|
|
59
|
+
base = os.environ.get("CLAUDE_CONFIG_DIR") or str(Path.home() / ".claude")
|
|
60
|
+
return Path(base).expanduser() / "settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_settings(path: Path) -> dict:
|
|
64
|
+
if not path.exists():
|
|
65
|
+
return {}
|
|
66
|
+
raw = path.read_text(encoding="utf-8")
|
|
67
|
+
if not raw.strip():
|
|
68
|
+
return {}
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(raw)
|
|
71
|
+
except json.JSONDecodeError as exc:
|
|
72
|
+
raise HookInstallError(
|
|
73
|
+
f"{path} is not valid JSON ({exc}). Refusing to edit it so your real "
|
|
74
|
+
"config is never truncated. Fix or remove the file, then retry."
|
|
75
|
+
) from exc
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
raise HookInstallError(f"{path}: top-level JSON is not an object; refusing to edit.")
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _command_text(handler: object) -> str:
|
|
82
|
+
return str(handler.get("command", "")) if isinstance(handler, dict) else ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_zeno_handler(handler: object) -> bool:
|
|
86
|
+
cmd = _command_text(handler)
|
|
87
|
+
return any(sig in cmd for sig in _ZENO_SIGNATURES)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _group_is_zeno(group: object) -> bool:
|
|
91
|
+
return isinstance(group, dict) and any(_is_zeno_handler(h) for h in group.get("hooks", []))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _timestamp() -> str:
|
|
95
|
+
# microseconds: avoid same-second backup-stamp collisions (a rapid re-install
|
|
96
|
+
# would otherwise overwrite the prior backup via shutil.copy2).
|
|
97
|
+
return datetime.now().strftime("%Y%m%dT%H%M%S%f")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _strip_zeno(groups: list) -> list:
|
|
101
|
+
"""Remove zeno-owned HANDLERS from each matcher group, keeping any co-located
|
|
102
|
+
non-zeno handler, and drop a group only once its hooks list is empty. (Filtering
|
|
103
|
+
at the group level would delete the whole group, taking a user's hand-co-located
|
|
104
|
+
handler with it.) Unchanged groups are reused by identity so callers can detect
|
|
105
|
+
'nothing changed' with ==.
|
|
106
|
+
"""
|
|
107
|
+
out: list = []
|
|
108
|
+
for g in groups:
|
|
109
|
+
if not isinstance(g, dict):
|
|
110
|
+
out.append(g)
|
|
111
|
+
continue
|
|
112
|
+
handlers = g.get("hooks")
|
|
113
|
+
if isinstance(handlers, list):
|
|
114
|
+
kept = [h for h in handlers if not _is_zeno_handler(h)]
|
|
115
|
+
if not kept:
|
|
116
|
+
continue # the whole group was zeno-only -> drop it
|
|
117
|
+
if len(kept) != len(handlers):
|
|
118
|
+
out.append({**g, "hooks": kept})
|
|
119
|
+
continue
|
|
120
|
+
out.append(g)
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _backup(path: Path, stamp: str) -> Path | None:
|
|
125
|
+
if not path.exists():
|
|
126
|
+
return None
|
|
127
|
+
bak = path.with_name(f"{path.name}.zeno-bak.{stamp}")
|
|
128
|
+
shutil.copy2(path, bak)
|
|
129
|
+
return bak
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _write(path: Path, data: dict) -> None:
|
|
133
|
+
# Atomic: write a sibling temp file then os.replace, so a crash mid-write can never
|
|
134
|
+
# truncate the user's live settings.json (the exact failure this module guards against).
|
|
135
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
tmp = path.with_name(f"{path.name}.zeno-tmp.{os.getpid()}")
|
|
137
|
+
tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
138
|
+
os.replace(tmp, path)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def install(
|
|
142
|
+
settings_path: Path,
|
|
143
|
+
*,
|
|
144
|
+
hook_command: str = DEFAULT_HOOK_COMMAND,
|
|
145
|
+
statusline_command: str | None = None,
|
|
146
|
+
force: bool = False,
|
|
147
|
+
stamp: str | None = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""Write zeno's capture hook (and optionally the statusLine) into settings.json.
|
|
150
|
+
|
|
151
|
+
Idempotent: any prior zeno-owned hook group is dropped and re-added, so
|
|
152
|
+
re-running never duplicates capture and migrates the legacy dotfiles
|
|
153
|
+
registration. ``statusline_command`` is set only if absent or already
|
|
154
|
+
zeno-owned, unless ``force`` (statusLine allows just one value).
|
|
155
|
+
"""
|
|
156
|
+
stamp = stamp or _timestamp()
|
|
157
|
+
data = _load_settings(settings_path)
|
|
158
|
+
command = f"{hook_command} # {ZENO_MARKER}"
|
|
159
|
+
|
|
160
|
+
hooks = data.setdefault("hooks", {})
|
|
161
|
+
if not isinstance(hooks, dict):
|
|
162
|
+
raise HookInstallError("settings.json 'hooks' is not an object; refusing to edit.")
|
|
163
|
+
for event in HOOK_EVENTS:
|
|
164
|
+
groups = hooks.setdefault(event, [])
|
|
165
|
+
if not isinstance(groups, list):
|
|
166
|
+
raise HookInstallError(f"settings.json hooks.{event} is not a list; refusing to edit.")
|
|
167
|
+
groups[:] = _strip_zeno(groups)
|
|
168
|
+
groups.append({"matcher": "*", "hooks": [{"type": "command", "command": command}]})
|
|
169
|
+
|
|
170
|
+
statusline_action = "unchanged"
|
|
171
|
+
if statusline_command is not None:
|
|
172
|
+
existing = data.get("statusLine")
|
|
173
|
+
if existing and not _is_zeno_handler(existing) and not force:
|
|
174
|
+
statusline_action = "skipped-existing"
|
|
175
|
+
else:
|
|
176
|
+
data["statusLine"] = {
|
|
177
|
+
"type": "command",
|
|
178
|
+
"command": f"{statusline_command} # {ZENO_MARKER}",
|
|
179
|
+
}
|
|
180
|
+
statusline_action = "set"
|
|
181
|
+
|
|
182
|
+
backup = _backup(settings_path, stamp)
|
|
183
|
+
_write(settings_path, data)
|
|
184
|
+
return {
|
|
185
|
+
"backup": str(backup) if backup else None,
|
|
186
|
+
"events": list(HOOK_EVENTS),
|
|
187
|
+
"statusline": statusline_action,
|
|
188
|
+
"command": command,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def uninstall(settings_path: Path, *, restore: bool = False, stamp: str | None = None) -> dict:
|
|
193
|
+
"""Remove only zeno-owned entries (or ``restore`` the latest backup wholesale)."""
|
|
194
|
+
stamp = stamp or _timestamp()
|
|
195
|
+
if restore:
|
|
196
|
+
baks = sorted(settings_path.parent.glob(f"{settings_path.name}.zeno-bak.*"))
|
|
197
|
+
if not baks:
|
|
198
|
+
raise HookInstallError(f"no zeno backup found next to {settings_path} to restore.")
|
|
199
|
+
latest = baks[-1]
|
|
200
|
+
shutil.copy2(latest, settings_path)
|
|
201
|
+
return {"restored_from": str(latest)}
|
|
202
|
+
|
|
203
|
+
data = _load_settings(settings_path)
|
|
204
|
+
removed_events: list[str] = []
|
|
205
|
+
hooks = data.get("hooks")
|
|
206
|
+
if isinstance(hooks, dict):
|
|
207
|
+
for event in list(hooks):
|
|
208
|
+
groups = hooks[event]
|
|
209
|
+
if not isinstance(groups, list):
|
|
210
|
+
continue
|
|
211
|
+
stripped = _strip_zeno(groups)
|
|
212
|
+
if stripped != groups:
|
|
213
|
+
removed_events.append(event)
|
|
214
|
+
groups[:] = stripped
|
|
215
|
+
if not groups:
|
|
216
|
+
del hooks[event]
|
|
217
|
+
if not hooks:
|
|
218
|
+
data.pop("hooks", None)
|
|
219
|
+
|
|
220
|
+
statusline_removed = False
|
|
221
|
+
if _is_zeno_handler(data.get("statusLine")):
|
|
222
|
+
data.pop("statusLine", None)
|
|
223
|
+
statusline_removed = True
|
|
224
|
+
|
|
225
|
+
backup = _backup(settings_path, stamp)
|
|
226
|
+
_write(settings_path, data)
|
|
227
|
+
return {
|
|
228
|
+
"backup": str(backup) if backup else None,
|
|
229
|
+
"removed_events": removed_events,
|
|
230
|
+
"statusline_removed": statusline_removed,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def status(settings_path: Path) -> dict:
|
|
235
|
+
"""Report which events are zeno-owned without mutating anything."""
|
|
236
|
+
data = _load_settings(settings_path)
|
|
237
|
+
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
|
238
|
+
installed = [ev for ev in HOOK_EVENTS if any(_group_is_zeno(g) for g in hooks.get(ev, []))]
|
|
239
|
+
return {
|
|
240
|
+
"settings_path": str(settings_path),
|
|
241
|
+
"events_installed": installed,
|
|
242
|
+
"all_events_installed": installed == list(HOOK_EVENTS),
|
|
243
|
+
"statusline_zeno": _is_zeno_handler(data.get("statusLine")),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _packaged_hook_path() -> Path:
|
|
248
|
+
from importlib import resources
|
|
249
|
+
|
|
250
|
+
return Path(str(resources.files("zeno_cli") / "_hooks" / "cc_bridge.py"))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def run() -> int:
|
|
254
|
+
"""Exec the bundled cc-bridge hook, passing stdin through.
|
|
255
|
+
|
|
256
|
+
Called by Claude Code on each registered event via ``zeno hook run``. NEVER
|
|
257
|
+
blocks Claude Code: if the bundled hook can't be located or exec'd, return 0.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
hook = _packaged_hook_path()
|
|
261
|
+
except (ModuleNotFoundError, FileNotFoundError, OSError):
|
|
262
|
+
return 0
|
|
263
|
+
if not hook.exists():
|
|
264
|
+
return 0
|
|
265
|
+
try:
|
|
266
|
+
os.execv(sys.executable, [sys.executable, str(hook)])
|
|
267
|
+
except OSError:
|
|
268
|
+
return 0
|
|
269
|
+
return 0 # unreachable once execv succeeds
|
zeno_cli/hud/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""zeno-hud: the Claude Code statusline (read surface) packaged with the CLI."""
|