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
zeno_sdk/client.py ADDED
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ._runtime import run_coro
10
+ from .config import ZenoConfig, load_config
11
+ from .daemon import DaemonManager
12
+ from .privacy import redact_metadata
13
+ from .session import SessionContext
14
+ from .storage import ZenoStorage
15
+ from .types import SupervisionEvent
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class Zeno:
20
+ project: str
21
+ project_root: Path | None = None
22
+ config: ZenoConfig | None = None
23
+ _storage: ZenoStorage | None = None
24
+ _daemon: DaemonManager | None = None
25
+
26
+ def __post_init__(self) -> None:
27
+ self.config = self.config or load_config(self.project_root)
28
+ self._storage = ZenoStorage(self.config.db_path)
29
+ self._daemon = DaemonManager(
30
+ db_path=self.config.db_path,
31
+ flush_interval_seconds=self.config.flush_interval_seconds,
32
+ )
33
+ run_coro(self._storage.migrate())
34
+
35
+ @property
36
+ def storage(self) -> ZenoStorage:
37
+ if self._storage is None:
38
+ raise RuntimeError("Storage is not initialized.")
39
+ return self._storage
40
+
41
+ @property
42
+ def daemon(self) -> DaemonManager:
43
+ if self._daemon is None:
44
+ raise RuntimeError("Daemon is not initialized.")
45
+ return self._daemon
46
+
47
+ def _project_id(self) -> uuid.UUID:
48
+ return uuid.uuid5(uuid.NAMESPACE_URL, self.project)
49
+
50
+ def session(self, *, harness: str) -> SessionContext:
51
+ self.daemon.start()
52
+ return SessionContext(
53
+ storage=self.storage,
54
+ project_id=self._project_id(),
55
+ harness=harness,
56
+ probes_enabled=self.config.probes_enabled,
57
+ record_content=self.config.record_content,
58
+ )
59
+
60
+ def record_event(
61
+ self,
62
+ *,
63
+ session_id: uuid.UUID,
64
+ type: str,
65
+ agent_run_id: uuid.UUID | None = None,
66
+ latency_ms_to_decide: int | None = None,
67
+ metadata: dict[str, Any] | None = None,
68
+ ) -> None:
69
+ self.daemon.start()
70
+ event = SupervisionEvent(
71
+ id=uuid.uuid4(),
72
+ session_id=session_id,
73
+ agent_run_id=agent_run_id,
74
+ type=type,
75
+ timestamp=datetime.now(tz=UTC),
76
+ latency_ms_to_decide=latency_ms_to_decide,
77
+ metadata=redact_metadata(metadata, record_content=self.config.record_content),
78
+ )
79
+ run_coro(self.storage.insert_event(event))
80
+
81
+ def prompt_load_probe(
82
+ self, *, session: SessionContext, subscales: dict[str, int] | None, skipped: bool = False
83
+ ) -> None:
84
+ session.prompt_load_probe(subscales=subscales, skipped=skipped)
85
+
86
+ def shutdown(self) -> None:
87
+ self.daemon.stop()
zeno_sdk/config.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class ZenoConfig:
12
+ base_dir: Path
13
+ db_path: Path
14
+ probes_enabled: bool
15
+ flush_interval_seconds: int
16
+ redaction_enabled: bool
17
+ record_content: bool
18
+
19
+
20
+ def _read_toml(path: Path) -> dict[str, Any]:
21
+ if not path.exists():
22
+ return {}
23
+ return tomllib.loads(path.read_text(encoding="utf-8"))
24
+
25
+
26
+ def load_config(project_root: Path | None = None) -> ZenoConfig:
27
+ home = Path.home()
28
+ base_dir = Path(os.getenv("ZENO_HOME", home / ".zeno")).expanduser()
29
+ base_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ user_config = _read_toml(base_dir / "config.toml")
32
+ project_config: dict[str, Any] = {}
33
+ if project_root is not None:
34
+ project_config = _read_toml(project_root / ".zeno.toml")
35
+
36
+ cfg = {}
37
+ cfg.update(user_config.get("zeno", {}))
38
+ cfg.update(project_config.get("zeno", {}))
39
+
40
+ probes_enabled = str(
41
+ os.getenv("ZENO_PROBES_ENABLED", cfg.get("probes_enabled", "true"))
42
+ ).lower()
43
+ redaction_enabled = str(
44
+ os.getenv("ZENO_REDACTION_ENABLED", cfg.get("redaction_enabled", "true"))
45
+ ).lower()
46
+ record_content = str(
47
+ os.getenv("ZENO_RECORD_CONTENT", cfg.get("record_content", "false"))
48
+ ).lower()
49
+ flush_interval = int(
50
+ os.getenv("ZENO_FLUSH_INTERVAL_SECONDS", cfg.get("flush_interval_seconds", 5))
51
+ )
52
+ db_path = Path(os.getenv("ZENO_DB_PATH", str(base_dir / "zeno.db"))).expanduser()
53
+
54
+ return ZenoConfig(
55
+ base_dir=base_dir,
56
+ db_path=db_path,
57
+ probes_enabled=probes_enabled in {"1", "true", "yes", "on"},
58
+ flush_interval_seconds=max(flush_interval, 1),
59
+ redaction_enabled=redaction_enabled in {"1", "true", "yes", "on"},
60
+ record_content=record_content in {"1", "true", "yes", "on"},
61
+ )
zeno_sdk/daemon.py ADDED
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import multiprocessing as mp
4
+ import signal
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from ._runtime import run_coro
11
+ from .storage import ZenoStorage, make_tax_point
12
+
13
+
14
+ def _daemon_main(db_path: str, flush_interval: int, stop_event: mp.Event) -> None:
15
+ running = True
16
+
17
+ def _handle_sigterm(_signum: int, _frame: object) -> None:
18
+ nonlocal running
19
+ running = False
20
+
21
+ signal.signal(signal.SIGTERM, _handle_sigterm)
22
+ storage = ZenoStorage(Path(db_path))
23
+ while running and not stop_event.is_set():
24
+ # Keep process alive and ready for future sync/aggregation jobs.
25
+ # Current v1 loop only runs periodic liveness cycles.
26
+ time.sleep(max(flush_interval, 1))
27
+ _ = storage
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class DaemonManager:
32
+ db_path: Path
33
+ flush_interval_seconds: int
34
+ _process: mp.Process | None = None
35
+ _stop_event: mp.Event | None = None
36
+
37
+ def start(self) -> None:
38
+ if self._process and self._process.is_alive():
39
+ return
40
+ self._stop_event = mp.Event()
41
+ self._process = mp.Process(
42
+ target=_daemon_main,
43
+ args=(str(self.db_path), self.flush_interval_seconds, self._stop_event),
44
+ daemon=True,
45
+ )
46
+ self._process.start()
47
+
48
+ def stop(self, timeout_seconds: float = 5.0) -> None:
49
+ if not self._process:
50
+ return
51
+ if self._stop_event:
52
+ self._stop_event.set()
53
+ self._process.join(timeout_seconds)
54
+ if self._process.is_alive():
55
+ self._process.terminate()
56
+ self._process.join(timeout_seconds)
57
+ self._process = None
58
+ self._stop_event = None
59
+
60
+ @property
61
+ def pid(self) -> int | None:
62
+ if self._process and self._process.is_alive():
63
+ return self._process.pid
64
+ return None
65
+
66
+
67
+ def compute_incremental_tax_point(storage: ZenoStorage, session_id: str) -> None:
68
+ stats = run_coro(storage.compute_session_stats(session_id))
69
+ point = make_tax_point(session_id, stats)
70
+ # Use stable deterministic id to keep updates idempotent.
71
+ point.id = uuid.uuid5(uuid.UUID(session_id), "tax-point")
72
+ run_coro(storage.upsert_tax_point(point))
zeno_sdk/privacy.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ BLOCKED_KEYS = {"prompt", "prompt_text", "output", "output_text", "completion", "response"}
9
+
10
+
11
+ def hash_text(value: str) -> str:
12
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
13
+
14
+
15
+ def redact_metadata(
16
+ metadata: Mapping[str, Any] | None,
17
+ *,
18
+ record_content: bool = False,
19
+ record_tool_args: bool = False,
20
+ ) -> dict[str, Any]:
21
+ if metadata is None:
22
+ return {}
23
+
24
+ redacted: dict[str, Any] = {}
25
+ for key, value in metadata.items():
26
+ lowered = key.lower()
27
+ if lowered in BLOCKED_KEYS and not record_content:
28
+ text = str(value)
29
+ redacted[f"{key}_hash"] = hash_text(text)
30
+ redacted[f"{key}_len"] = len(text)
31
+ continue
32
+
33
+ if lowered in {"tool_args", "arguments"} and not record_tool_args:
34
+ if isinstance(value, Mapping):
35
+ redacted["tool_arg_keys"] = sorted(value.keys())
36
+ else:
37
+ redacted["tool_arg_keys"] = []
38
+ continue
39
+
40
+ if isinstance(value, (str, int, float, bool)) or value is None:
41
+ redacted[key] = value
42
+ continue
43
+
44
+ # Keep complex values serializable and compact.
45
+ redacted[key] = json.loads(json.dumps(value, default=str))
46
+ return redacted
zeno_sdk/session.py ADDED
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+ from zeno_core.tlx_s import run_tlx_survey_tui, should_prompt_probe
9
+
10
+ from ._runtime import run_coro
11
+ from .daemon import compute_incremental_tax_point
12
+ from .privacy import redact_metadata
13
+ from .storage import ZenoStorage
14
+ from .types import AgentRun, LoadProbe, LoadProbeSubscales, Session, SupervisionEvent
15
+
16
+
17
+ def _now() -> datetime:
18
+ return datetime.now(tz=UTC)
19
+
20
+
21
+ _probe_state: dict[str, Any] = {
22
+ "sessions_since_last_probe": 0,
23
+ "last_probe_at": None,
24
+ }
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class AgentRunContext:
29
+ storage: ZenoStorage
30
+ session_id: uuid.UUID
31
+ harness: str
32
+ model: str
33
+ run_id: uuid.UUID
34
+ _outcome: str = "unknown"
35
+ _started_at: datetime | None = None
36
+ _ended_at: datetime | None = None
37
+
38
+ def outcome(self, value: str) -> None:
39
+ self._outcome = value
40
+
41
+ def __enter__(self) -> AgentRunContext:
42
+ self._started_at = _now()
43
+ run = AgentRun(
44
+ id=self.run_id,
45
+ session_id=self.session_id,
46
+ harness=self.harness,
47
+ model=self.model,
48
+ started_at=self._started_at,
49
+ ended_at=None,
50
+ outcome="unknown",
51
+ )
52
+ run_coro(self.storage.insert_agent_run(run))
53
+ return self
54
+
55
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
56
+ self._ended_at = _now()
57
+ if self._started_at is None:
58
+ self._started_at = self._ended_at
59
+ final = AgentRun(
60
+ id=self.run_id,
61
+ session_id=self.session_id,
62
+ harness=self.harness,
63
+ model=self.model,
64
+ started_at=self._started_at,
65
+ ended_at=self._ended_at,
66
+ outcome=self._outcome,
67
+ )
68
+ run_coro(self.storage.insert_agent_run(final))
69
+
70
+
71
+ @dataclass(slots=True)
72
+ class SessionContext:
73
+ storage: ZenoStorage
74
+ project_id: uuid.UUID
75
+ harness: str
76
+ probes_enabled: bool
77
+ record_content: bool
78
+ session_id: uuid.UUID | None = None
79
+ _started_at: datetime | None = None
80
+
81
+ def __enter__(self) -> SessionContext:
82
+ self.session_id = uuid.uuid4()
83
+ self._started_at = _now()
84
+ session = Session(
85
+ id=self.session_id,
86
+ start_at=self._started_at,
87
+ end_at=None,
88
+ agent_count_max=1,
89
+ harness=self.harness,
90
+ project_id=self.project_id,
91
+ )
92
+ run_coro(self.storage.insert_session(session))
93
+ return self
94
+
95
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
96
+ if self.session_id is None or self._started_at is None:
97
+ return
98
+
99
+ stats = run_coro(self.storage.compute_session_stats(str(self.session_id)))
100
+ finished = Session(
101
+ id=self.session_id,
102
+ start_at=self._started_at,
103
+ end_at=_now(),
104
+ agent_count_max=max(1, int(round(stats.avg_n_agents)) or 1),
105
+ harness=self.harness,
106
+ project_id=self.project_id,
107
+ )
108
+ run_coro(self.storage.insert_session(finished))
109
+ compute_incremental_tax_point(self.storage, str(self.session_id))
110
+ _probe_state["sessions_since_last_probe"] += 1
111
+ last_probe_at = _probe_state.get("last_probe_at")
112
+ seconds_since = None
113
+ if isinstance(last_probe_at, datetime):
114
+ seconds_since = (_now() - last_probe_at).total_seconds()
115
+ should_prompt = should_prompt_probe(
116
+ session_duration_minutes=(finished.end_at - finished.start_at).total_seconds() / 60,
117
+ sessions_since_last_probe=int(_probe_state["sessions_since_last_probe"]),
118
+ probes_enabled=self.probes_enabled,
119
+ seconds_since_last_probe=seconds_since,
120
+ active_agent_runs=0,
121
+ )
122
+ if should_prompt:
123
+ result = run_tlx_survey_tui()
124
+ self.prompt_load_probe(result.subscales, skipped=result.skipped)
125
+ if result.comment_hash:
126
+ self.record(type="intervene", metadata={"tlx_comment_hash": result.comment_hash})
127
+ _probe_state["sessions_since_last_probe"] = 0
128
+ _probe_state["last_probe_at"] = _now()
129
+
130
+ def agent_run(self, *, model: str) -> AgentRunContext:
131
+ if self.session_id is None:
132
+ raise RuntimeError("Session is not active.")
133
+ return AgentRunContext(
134
+ storage=self.storage,
135
+ session_id=self.session_id,
136
+ harness=self.harness,
137
+ model=model,
138
+ run_id=uuid.uuid4(),
139
+ )
140
+
141
+ def record(
142
+ self,
143
+ *,
144
+ type: str,
145
+ agent_run_id: uuid.UUID | None = None,
146
+ latency_ms_to_decide: int | None = None,
147
+ metadata: dict[str, Any] | None = None,
148
+ **extra_metadata: Any,
149
+ ) -> None:
150
+ if self.session_id is None:
151
+ raise RuntimeError("Session is not active.")
152
+ merged_metadata = dict(metadata or {})
153
+ merged_metadata.update(extra_metadata)
154
+ event = SupervisionEvent(
155
+ id=uuid.uuid4(),
156
+ session_id=self.session_id,
157
+ agent_run_id=agent_run_id,
158
+ type=type,
159
+ timestamp=_now(),
160
+ latency_ms_to_decide=latency_ms_to_decide,
161
+ metadata=redact_metadata(merged_metadata, record_content=self.record_content),
162
+ )
163
+ run_coro(self.storage.insert_event(event))
164
+
165
+ def prompt_load_probe(self, subscales: dict[str, int] | None, skipped: bool = False) -> None:
166
+ if self.session_id is None:
167
+ raise RuntimeError("Session is not active.")
168
+ if not self.probes_enabled and not skipped:
169
+ return
170
+ prompted = _now()
171
+ probe = LoadProbe(
172
+ id=uuid.uuid4(),
173
+ session_id=self.session_id,
174
+ prompted_at=prompted,
175
+ responded_at=None if skipped else prompted,
176
+ skipped=skipped,
177
+ subscales=LoadProbeSubscales(**subscales) if subscales is not None else None,
178
+ )
179
+ run_coro(self.storage.insert_load_probe(probe))