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,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
@@ -0,0 +1 @@
1
+ """zeno-hud: the Claude Code statusline (read surface) packaged with the CLI."""