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,33 @@
1
+ from __future__ import annotations
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+
6
+ config = context.config
7
+
8
+
9
+ def run_migrations_offline() -> None:
10
+ url = config.get_main_option("sqlalchemy.url")
11
+ context.configure(url=url, literal_binds=True, compare_type=True)
12
+
13
+ with context.begin_transaction():
14
+ context.run_migrations()
15
+
16
+
17
+ def run_migrations_online() -> None:
18
+ connectable = engine_from_config(
19
+ config.get_section(config.config_ini_section) or {},
20
+ prefix="sqlalchemy.",
21
+ poolclass=pool.NullPool,
22
+ )
23
+
24
+ with connectable.connect() as connection:
25
+ context.configure(connection=connection, compare_type=True)
26
+ with context.begin_transaction():
27
+ context.run_migrations()
28
+
29
+
30
+ if context.is_offline_mode():
31
+ run_migrations_offline()
32
+ else:
33
+ run_migrations_online()
@@ -0,0 +1,18 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+ ${imports if imports else ""}
11
+
12
+
13
+ def upgrade() -> None:
14
+ ${upgrades if upgrades else "pass"}
15
+
16
+
17
+ def downgrade() -> None:
18
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,79 @@
1
+ """initial schema
2
+
3
+ Revision ID: 0001_initial
4
+ Revises:
5
+ Create Date: 2026-04-30
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "0001_initial"
15
+ down_revision = None
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ op.create_table(
22
+ "sessions",
23
+ sa.Column("id", sa.String(), primary_key=True),
24
+ sa.Column("start_at", sa.String(), nullable=False),
25
+ sa.Column("end_at", sa.String(), nullable=True),
26
+ sa.Column("agent_count_max", sa.Integer(), nullable=False),
27
+ sa.Column("harness", sa.String(), nullable=False),
28
+ sa.Column("project_id", sa.String(), nullable=False),
29
+ sa.Column("synced_at", sa.String(), nullable=True),
30
+ )
31
+ op.create_table(
32
+ "agent_runs",
33
+ sa.Column("id", sa.String(), primary_key=True),
34
+ sa.Column("session_id", sa.String(), nullable=False, index=True),
35
+ sa.Column("harness", sa.String(), nullable=False),
36
+ sa.Column("model", sa.String(), nullable=False),
37
+ sa.Column("started_at", sa.String(), nullable=False),
38
+ sa.Column("ended_at", sa.String(), nullable=True),
39
+ sa.Column("outcome", sa.String(), nullable=False),
40
+ sa.Column("synced_at", sa.String(), nullable=True),
41
+ )
42
+ op.create_table(
43
+ "supervision_events",
44
+ sa.Column("id", sa.String(), primary_key=True),
45
+ sa.Column("session_id", sa.String(), nullable=False, index=True),
46
+ sa.Column("agent_run_id", sa.String(), nullable=True, index=True),
47
+ sa.Column("type", sa.String(), nullable=False),
48
+ sa.Column("timestamp", sa.String(), nullable=False),
49
+ sa.Column("latency_ms_to_decide", sa.Integer(), nullable=True),
50
+ sa.Column("metadata_json", sa.Text(), nullable=False, server_default="{}"),
51
+ sa.Column("synced_at", sa.String(), nullable=True),
52
+ )
53
+ op.create_table(
54
+ "load_probes",
55
+ sa.Column("id", sa.String(), primary_key=True),
56
+ sa.Column("session_id", sa.String(), nullable=False, index=True),
57
+ sa.Column("prompted_at", sa.String(), nullable=False),
58
+ sa.Column("responded_at", sa.String(), nullable=True),
59
+ sa.Column("skipped", sa.Integer(), nullable=False),
60
+ sa.Column("subscales_json", sa.Text(), nullable=True),
61
+ sa.Column("synced_at", sa.String(), nullable=True),
62
+ )
63
+ op.create_table(
64
+ "babysitting_tax_points",
65
+ sa.Column("id", sa.String(), primary_key=True),
66
+ sa.Column("session_id", sa.String(), nullable=False, index=True),
67
+ sa.Column("n_agents_active", sa.Integer(), nullable=False),
68
+ sa.Column("composite_load", sa.Float(), nullable=False),
69
+ sa.Column("output_quality", sa.Float(), nullable=True),
70
+ sa.Column("synced_at", sa.String(), nullable=True),
71
+ )
72
+
73
+
74
+ def downgrade() -> None:
75
+ op.drop_table("babysitting_tax_points")
76
+ op.drop_table("load_probes")
77
+ op.drop_table("supervision_events")
78
+ op.drop_table("agent_runs")
79
+ op.drop_table("sessions")
@@ -0,0 +1,53 @@
1
+ """cognition_samples: per-turn cognition stream (tokens + context + attention)
2
+
3
+ Revision ID: 0002_cognition_samples
4
+ Revises: 0001_initial
5
+ Create Date: 2026-06-16
6
+
7
+ One row per assistant turn, written by the HUD statusline (the read+write
8
+ surface) so the in-terminal HUD and the tailnet dashboard share the exact
9
+ same attention + token signal. See docs/COGNITION_UNIFIED_PLAN.md.
10
+
11
+ Kept deliberately schema-compatible with the self-create block in
12
+ integrations/zeno-hud/zeno_hud.py and dotfiles/claude/hooks/zeno-cc-bridge.py:
13
+ if you add a column here, mirror it in those stdlib writers (they never
14
+ import this package, to keep hook/statusline startup sub-100ms).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sqlalchemy as sa
20
+ from alembic import op
21
+
22
+ # revision identifiers, used by Alembic.
23
+ revision = "0002_cognition_samples"
24
+ down_revision = "0001_initial"
25
+ branch_labels = None
26
+ depends_on = None
27
+
28
+
29
+ def upgrade() -> None:
30
+ op.create_table(
31
+ "cognition_samples",
32
+ sa.Column("id", sa.String(), primary_key=True),
33
+ sa.Column("session_id", sa.String(), nullable=False, index=True),
34
+ sa.Column("ts", sa.String(), nullable=False),
35
+ # tokens + context captured from the statusline stdin
36
+ sa.Column("context_pct", sa.Float(), nullable=True),
37
+ sa.Column("input_tokens", sa.Integer(), nullable=True),
38
+ sa.Column("output_tokens", sa.Integer(), nullable=True),
39
+ sa.Column("cache_read_tokens", sa.Integer(), nullable=True),
40
+ sa.Column("cache_creation_tokens", sa.Integer(), nullable=True),
41
+ sa.Column("total_tokens", sa.Integer(), nullable=True),
42
+ # attention signal (score 0..100 + its components) from the shared model
43
+ sa.Column("attention_score", sa.Integer(), nullable=True),
44
+ sa.Column("attention_effort", sa.Float(), nullable=True),
45
+ sa.Column("attention_deliberation", sa.Float(), nullable=True),
46
+ sa.Column("attention_trend", sa.Float(), nullable=True),
47
+ sa.Column("model", sa.String(), nullable=True),
48
+ sa.Column("harness", sa.String(), nullable=True),
49
+ )
50
+
51
+
52
+ def downgrade() -> None:
53
+ op.drop_table("cognition_samples")
@@ -0,0 +1,41 @@
1
+ """cognition_samples v2: multi-dimensional driver columns
2
+
3
+ Revision ID: 0003_cognition_drivers
4
+ Revises: 0002_cognition_samples
5
+ Create Date: 2026-06-16
6
+
7
+ HUD v2 turns the single attention score into a composite of five named drivers.
8
+ This adds the four new driver columns (attention_effort already exists from 0002).
9
+
10
+ Kept in lockstep with the canonical schema in
11
+ integrations/zeno-hud/zeno_cognition.py (SAMPLE_COLUMNS) and the stdlib writers
12
+ (zeno_hud.py + dotfiles zeno-cc-bridge.py call zeno_cognition.ensure_schema, which
13
+ ADDs these columns on existing DBs). The schema-drift test guards the two lists.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sqlalchemy as sa
19
+ from alembic import op
20
+
21
+ revision = "0003_cognition_drivers"
22
+ down_revision = "0002_cognition_samples"
23
+ branch_labels = None
24
+ depends_on = None
25
+
26
+ _DRIVER_COLUMNS = (
27
+ "attention_autonomy",
28
+ "attention_verification",
29
+ "attention_fatigue",
30
+ "attention_flow",
31
+ )
32
+
33
+
34
+ def upgrade() -> None:
35
+ for col in _DRIVER_COLUMNS:
36
+ op.add_column("cognition_samples", sa.Column(col, sa.Float(), nullable=True))
37
+
38
+
39
+ def downgrade() -> None:
40
+ for col in reversed(_DRIVER_COLUMNS):
41
+ op.drop_column("cognition_samples", col)
@@ -0,0 +1,248 @@
1
+ """transcript intelligence: multi-tool session capture + cache-aware token/cost ledger
2
+
3
+ Revision ID: 0004_transcript_intelligence
4
+ Revises: 0003_cognition_drivers
5
+ Create Date: 2026-06-17
6
+
7
+ Additive session-intelligence tables, adopted from agentsview (MIT, (c) 2026 Kenn
8
+ Software LLC; see THIRD_PARTY_LICENSES.md). These are a derived cache of coding-agent
9
+ transcripts (Claude Code, Codex, Cursor) scoring *agent* behavior - token/cost, tool
10
+ health, outcomes - ALONGSIDE zeno's primary, untouched human-cognition signal in
11
+ ``cognition_samples``. agentsview's ``sessions``/``messages`` collide with zeno's
12
+ existing tables, so they are namespaced ``transcript_*``.
13
+
14
+ Kept in lockstep with the canonical stdlib schema in
15
+ ``packages/session-intel/src/zeno_session_intel/schema.py`` (the ingester writes via
16
+ stdlib sqlite3 and calls ``ensure_schema``; this migration is the SDK-managed-DB path).
17
+ The schema-drift test (``tests/test_session_intel_schema.py``) guards the two lists.
18
+
19
+ FTS5 is created via raw SQL (alembic does not model virtual tables) and is guarded:
20
+ on a SQLite build without the fts5 module it is skipped, search degrades, ingest still
21
+ works. ``transcript_messages.id`` is a synthetic INTEGER rowid - it is the FTS5
22
+ ``content_rowid``; do not make it a composite PK.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import sqlalchemy as sa
28
+ from alembic import op
29
+
30
+ revision = "0004_transcript_intelligence"
31
+ down_revision = "0003_cognition_drivers"
32
+ branch_labels = None
33
+ depends_on = None
34
+
35
+ _NOW = sa.text("(strftime('%Y-%m-%dT%H:%M:%fZ','now'))")
36
+
37
+ _FTS_SQL = """
38
+ CREATE VIRTUAL TABLE IF NOT EXISTS transcript_messages_fts USING fts5(
39
+ content,
40
+ content='transcript_messages',
41
+ content_rowid='id',
42
+ tokenize='porter unicode61'
43
+ );
44
+ CREATE TRIGGER IF NOT EXISTS transcript_messages_ai
45
+ AFTER INSERT ON transcript_messages BEGIN
46
+ INSERT INTO transcript_messages_fts(rowid, content) VALUES (new.id, new.content);
47
+ END;
48
+ CREATE TRIGGER IF NOT EXISTS transcript_messages_ad
49
+ AFTER DELETE ON transcript_messages BEGIN
50
+ INSERT INTO transcript_messages_fts(transcript_messages_fts, rowid, content)
51
+ VALUES ('delete', old.id, old.content);
52
+ END;
53
+ CREATE TRIGGER IF NOT EXISTS transcript_messages_au
54
+ AFTER UPDATE ON transcript_messages BEGIN
55
+ INSERT INTO transcript_messages_fts(transcript_messages_fts, rowid, content)
56
+ VALUES ('delete', old.id, old.content);
57
+ INSERT INTO transcript_messages_fts(rowid, content) VALUES (new.id, new.content);
58
+ END;
59
+ """
60
+
61
+
62
+ def upgrade() -> None:
63
+ op.create_table(
64
+ "transcript_sessions",
65
+ sa.Column("id", sa.String(), primary_key=True),
66
+ sa.Column("zeno_session_id", sa.String(), nullable=True),
67
+ sa.Column("agent", sa.String(), nullable=False, server_default="claude"),
68
+ sa.Column("project", sa.String(), nullable=False, server_default=""),
69
+ sa.Column("machine", sa.String(), nullable=False, server_default="local"),
70
+ sa.Column("first_message", sa.Text(), nullable=True),
71
+ sa.Column("display_name", sa.String(), nullable=True),
72
+ sa.Column("started_at", sa.String(), nullable=True),
73
+ sa.Column("ended_at", sa.String(), nullable=True),
74
+ sa.Column("message_count", sa.Integer(), nullable=False, server_default="0"),
75
+ sa.Column("user_message_count", sa.Integer(), nullable=False, server_default="0"),
76
+ sa.Column("total_output_tokens", sa.Integer(), nullable=False, server_default="0"),
77
+ sa.Column("peak_context_tokens", sa.Integer(), nullable=False, server_default="0"),
78
+ sa.Column("is_automated", sa.Integer(), nullable=False, server_default="0"),
79
+ sa.Column("tool_failure_signal_count", sa.Integer(), nullable=False, server_default="0"),
80
+ sa.Column("tool_retry_count", sa.Integer(), nullable=False, server_default="0"),
81
+ sa.Column("edit_churn_count", sa.Integer(), nullable=False, server_default="0"),
82
+ sa.Column("consecutive_failure_max", sa.Integer(), nullable=False, server_default="0"),
83
+ sa.Column("outcome", sa.String(), nullable=False, server_default="unknown"),
84
+ sa.Column("outcome_confidence", sa.String(), nullable=False, server_default="low"),
85
+ sa.Column("ended_with_role", sa.String(), nullable=False, server_default=""),
86
+ sa.Column("final_failure_streak", sa.Integer(), nullable=False, server_default="0"),
87
+ sa.Column("compaction_count", sa.Integer(), nullable=False, server_default="0"),
88
+ sa.Column("mid_task_compaction_count", sa.Integer(), nullable=False, server_default="0"),
89
+ sa.Column("context_pressure_max", sa.Float(), nullable=True),
90
+ sa.Column("health_score", sa.Integer(), nullable=True),
91
+ sa.Column("health_grade", sa.String(), nullable=True),
92
+ sa.Column("has_tool_calls", sa.Integer(), nullable=False, server_default="0"),
93
+ sa.Column("has_context_data", sa.Integer(), nullable=False, server_default="0"),
94
+ sa.Column("cwd", sa.String(), nullable=False, server_default=""),
95
+ sa.Column("git_branch", sa.String(), nullable=False, server_default=""),
96
+ sa.Column("file_path", sa.String(), nullable=True),
97
+ sa.Column("file_mtime", sa.Integer(), nullable=True),
98
+ sa.Column("content_hash", sa.String(), nullable=True),
99
+ sa.Column("created_at", sa.String(), nullable=False, server_default=_NOW),
100
+ )
101
+ op.create_index(
102
+ "idx_transcript_sessions_ended",
103
+ "transcript_sessions",
104
+ [sa.text("ended_at DESC"), "id"],
105
+ )
106
+ op.create_index("idx_transcript_sessions_agent", "transcript_sessions", ["agent"])
107
+ op.create_index("idx_transcript_sessions_project", "transcript_sessions", ["project"])
108
+ op.create_index("idx_transcript_sessions_zeno", "transcript_sessions", ["zeno_session_id"])
109
+
110
+ op.create_table(
111
+ "transcript_messages",
112
+ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
113
+ sa.Column(
114
+ "session_id",
115
+ sa.String(),
116
+ sa.ForeignKey("transcript_sessions.id", ondelete="CASCADE"),
117
+ nullable=False,
118
+ ),
119
+ sa.Column("ordinal", sa.Integer(), nullable=False),
120
+ sa.Column("role", sa.String(), nullable=False),
121
+ sa.Column("content", sa.Text(), nullable=False),
122
+ sa.Column("thinking_text", sa.Text(), nullable=False, server_default=""),
123
+ sa.Column("timestamp", sa.String(), nullable=True),
124
+ sa.Column("has_thinking", sa.Integer(), nullable=False, server_default="0"),
125
+ sa.Column("has_tool_use", sa.Integer(), nullable=False, server_default="0"),
126
+ sa.Column("content_length", sa.Integer(), nullable=False, server_default="0"),
127
+ sa.Column("model", sa.String(), nullable=False, server_default=""),
128
+ sa.Column("token_usage", sa.Text(), nullable=False, server_default=""),
129
+ sa.Column("context_tokens", sa.Integer(), nullable=False, server_default="0"),
130
+ sa.Column("output_tokens", sa.Integer(), nullable=False, server_default="0"),
131
+ sa.Column("source_uuid", sa.String(), nullable=False, server_default=""),
132
+ sa.Column("source_parent_uuid", sa.String(), nullable=False, server_default=""),
133
+ sa.Column("is_sidechain", sa.Integer(), nullable=False, server_default="0"),
134
+ sa.Column("is_compact_boundary", sa.Integer(), nullable=False, server_default="0"),
135
+ )
136
+ op.create_index(
137
+ "idx_transcript_messages_session_ordinal",
138
+ "transcript_messages",
139
+ ["session_id", "ordinal"],
140
+ unique=True,
141
+ )
142
+ op.create_index(
143
+ "idx_transcript_messages_session_role",
144
+ "transcript_messages",
145
+ ["session_id", "role"],
146
+ )
147
+
148
+ op.create_table(
149
+ "transcript_tool_calls",
150
+ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
151
+ sa.Column(
152
+ "session_id",
153
+ sa.String(),
154
+ sa.ForeignKey("transcript_sessions.id", ondelete="CASCADE"),
155
+ nullable=False,
156
+ ),
157
+ sa.Column("message_ordinal", sa.Integer(), nullable=False, server_default="0"),
158
+ sa.Column("ordinal_in_message", sa.Integer(), nullable=False, server_default="0"),
159
+ sa.Column("tool_name", sa.String(), nullable=False, server_default=""),
160
+ sa.Column("category", sa.String(), nullable=False, server_default="Other"),
161
+ sa.Column("is_error", sa.Integer(), nullable=False, server_default="0"),
162
+ sa.Column("source", sa.String(), nullable=False, server_default=""),
163
+ )
164
+ op.create_index("idx_transcript_tool_calls_session", "transcript_tool_calls", ["session_id"])
165
+ op.create_index("idx_transcript_tool_calls_category", "transcript_tool_calls", ["category"])
166
+
167
+ op.create_table(
168
+ "token_usage_events",
169
+ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
170
+ sa.Column(
171
+ "session_id",
172
+ sa.String(),
173
+ sa.ForeignKey("transcript_sessions.id", ondelete="CASCADE"),
174
+ nullable=False,
175
+ ),
176
+ sa.Column("message_ordinal", sa.Integer(), nullable=True),
177
+ sa.Column("source", sa.String(), nullable=False),
178
+ sa.Column("model", sa.String(), nullable=False),
179
+ sa.Column("input_tokens", sa.Integer(), nullable=False, server_default="0"),
180
+ sa.Column("output_tokens", sa.Integer(), nullable=False, server_default="0"),
181
+ sa.Column("cache_creation_input_tokens", sa.Integer(), nullable=False, server_default="0"),
182
+ sa.Column("cache_read_input_tokens", sa.Integer(), nullable=False, server_default="0"),
183
+ sa.Column("reasoning_tokens", sa.Integer(), nullable=False, server_default="0"),
184
+ sa.Column("cost_usd", sa.Float(), nullable=True),
185
+ sa.Column("cost_status", sa.String(), nullable=False, server_default=""),
186
+ sa.Column("cost_source", sa.String(), nullable=False, server_default=""),
187
+ sa.Column("occurred_at", sa.String(), nullable=True),
188
+ sa.Column("dedup_key", sa.String(), nullable=False, server_default=""),
189
+ )
190
+ op.create_index(
191
+ "idx_token_usage_events_dedup",
192
+ "token_usage_events",
193
+ ["session_id", "source", "dedup_key"],
194
+ unique=True,
195
+ # the partial predicate must survive on BOTH dialects, else Postgres builds a
196
+ # FULL unique index and id-less (empty dedup_key) session events collide there.
197
+ sqlite_where=sa.text("dedup_key != ''"),
198
+ postgresql_where=sa.text("dedup_key != ''"),
199
+ )
200
+ op.create_index("idx_token_usage_events_session", "token_usage_events", ["session_id"])
201
+ op.create_index("idx_token_usage_events_occurred", "token_usage_events", ["occurred_at"])
202
+
203
+ op.create_table(
204
+ "model_pricing",
205
+ sa.Column("model_pattern", sa.String(), primary_key=True),
206
+ sa.Column("input_per_mtok", sa.Float(), nullable=False, server_default="0"),
207
+ sa.Column("output_per_mtok", sa.Float(), nullable=False, server_default="0"),
208
+ sa.Column("cache_creation_per_mtok", sa.Float(), nullable=False, server_default="0"),
209
+ sa.Column("cache_read_per_mtok", sa.Float(), nullable=False, server_default="0"),
210
+ sa.Column("context_window", sa.Integer(), nullable=True),
211
+ sa.Column("updated_at", sa.String(), nullable=False, server_default=_NOW),
212
+ )
213
+
214
+ # FTS5 virtual table + sync triggers. SQLite-only (Postgres has no fts5; the
215
+ # local-first ingester never targets prod Postgres) and guarded for SQLite builds
216
+ # compiled without the fts5 module. Uses the raw driver connection's executescript
217
+ # because the trigger bodies contain semicolons that a naive split would break.
218
+ bind = op.get_bind()
219
+ if bind.dialect.name == "sqlite":
220
+ raw = bind.connection.driver_connection # the underlying sqlite3.Connection
221
+ try:
222
+ raw.execute("CREATE VIRTUAL TABLE temp._zeno_fts5_probe USING fts5(x)")
223
+ raw.execute("DROP TABLE temp._zeno_fts5_probe")
224
+ raw.executescript(_FTS_SQL)
225
+ except Exception:
226
+ # fts5 module unavailable in this SQLite build; search degrades, ingest works.
227
+ pass
228
+
229
+
230
+ def downgrade() -> None:
231
+ bind = op.get_bind()
232
+ if bind.dialect.name == "sqlite":
233
+ raw = bind.connection.driver_connection
234
+ for stmt in (
235
+ "DROP TRIGGER IF EXISTS transcript_messages_au",
236
+ "DROP TRIGGER IF EXISTS transcript_messages_ad",
237
+ "DROP TRIGGER IF EXISTS transcript_messages_ai",
238
+ "DROP TABLE IF EXISTS transcript_messages_fts",
239
+ ):
240
+ try:
241
+ raw.execute(stmt)
242
+ except Exception:
243
+ pass
244
+ op.drop_table("model_pricing")
245
+ op.drop_table("token_usage_events")
246
+ op.drop_table("transcript_tool_calls")
247
+ op.drop_table("transcript_messages")
248
+ op.drop_table("transcript_sessions")
@@ -0,0 +1,35 @@
1
+ [alembic]
2
+ script_location = alembic
3
+ sqlalchemy.url = sqlite:///placeholder.db
4
+
5
+ [loggers]
6
+ keys = root,sqlalchemy,alembic
7
+
8
+ [handlers]
9
+ keys = console
10
+
11
+ [formatters]
12
+ keys = generic
13
+
14
+ [logger_root]
15
+ level = WARN
16
+ handlers = console
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
zeno_sdk/_runtime.py ADDED
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Coroutine
5
+
6
+
7
+ def run_coro[T](coro: Coroutine[object, object, T]) -> T:
8
+ try:
9
+ asyncio.get_running_loop()
10
+ except RuntimeError:
11
+ return asyncio.run(coro)
12
+ raise RuntimeError("Zeno sync API called inside running event loop; use async entrypoints.")
@@ -0,0 +1,15 @@
1
+ from .anthropic import instrument as instrument_anthropic
2
+ from .claude_code import instrument as instrument_claude_code
3
+ from .crewai import instrument_agent as instrument_crewai_agent
4
+ from .crewai import instrument_crew as instrument_crewai_crew
5
+ from .langgraph import instrument as instrument_langgraph
6
+ from .openai import instrument as instrument_openai
7
+
8
+ __all__ = [
9
+ "instrument_anthropic",
10
+ "instrument_claude_code",
11
+ "instrument_crewai_agent",
12
+ "instrument_crewai_crew",
13
+ "instrument_langgraph",
14
+ "instrument_openai",
15
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from zeno_adapters.anthropic import instrument
4
+
5
+ __all__ = ["instrument"]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from zeno_adapters.claude_code import instrument
4
+
5
+ __all__ = ["instrument"]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from zeno_adapters.crewai import instrument_agent, instrument_crew
4
+
5
+ __all__ = ["instrument_agent", "instrument_crew"]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from zeno_adapters.langgraph import instrument
4
+
5
+ __all__ = ["instrument"]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from zeno_adapters.openai import instrument
4
+
5
+ __all__ = ["instrument"]
zeno_sdk/auth.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import keyring
4
+ from keyring.errors import KeyringError
5
+
6
+ SERVICE = "zeno-sdk"
7
+ TOKEN_KEY = "clerk_jwt"
8
+
9
+
10
+ def get_token() -> str | None:
11
+ try:
12
+ return keyring.get_password(SERVICE, TOKEN_KEY)
13
+ except KeyringError:
14
+ return None
15
+
16
+
17
+ def set_token(token: str) -> None:
18
+ keyring.set_password(SERVICE, TOKEN_KEY, token)
19
+
20
+
21
+ def clear_token() -> None:
22
+ try:
23
+ keyring.delete_password(SERVICE, TOKEN_KEY)
24
+ except KeyringError:
25
+ return