zeno-cli 0.3.4__tar.gz
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_cli-0.3.4/.gitignore +46 -0
- zeno_cli-0.3.4/PKG-INFO +161 -0
- zeno_cli-0.3.4/README.md +134 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/__init__.py +17 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/_common.py +38 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/anthropic.py +68 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/claude_code.py +101 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/crewai.py +92 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/langgraph.py +49 -0
- zeno_cli-0.3.4/_vendor_build/zeno_adapters/openai.py +108 -0
- zeno_cli-0.3.4/_vendor_build/zeno_core/__init__.py +67 -0
- zeno_cli-0.3.4/_vendor_build/zeno_core/analytics.py +193 -0
- zeno_cli-0.3.4/_vendor_build/zeno_core/rtlx_s.py +460 -0
- zeno_cli-0.3.4/_vendor_build/zeno_core/streak.py +178 -0
- zeno_cli-0.3.4/_vendor_build/zeno_core/tlx_s.py +192 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/__init__.py +6 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_generated/__init__.py +6 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_generated/client.py +819 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/env.py +33 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/script.py.mako +18 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_migrations/alembic.ini +35 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/_runtime.py +12 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/__init__.py +15 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/anthropic.py +5 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/claude_code.py +5 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/crewai.py +5 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/langgraph.py +5 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/adapters/openai.py +5 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/auth.py +25 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/client.py +87 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/config.py +61 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/daemon.py +72 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/privacy.py +46 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/session.py +179 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/storage.py +487 -0
- zeno_cli-0.3.4/_vendor_build/zeno_sdk/types/__init__.py +121 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/__init__.py +19 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/analytics.py +588 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/compression.py +123 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/ingest.py +376 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/model.py +129 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/parsers/__init__.py +31 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/parsers/claude_code.py +169 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/parsers/codex.py +265 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/parsers/cursor.py +198 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/prices.py +281 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/schema.py +277 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/signals.py +319 -0
- zeno_cli-0.3.4/_vendor_build/zeno_session_intel/taxonomy.py +71 -0
- zeno_cli-0.3.4/data/outreach_suppression.txt +14 -0
- zeno_cli-0.3.4/hatch_build.py +155 -0
- zeno_cli-0.3.4/pyproject.toml +87 -0
- zeno_cli-0.3.4/scripts/install-smoke.sh +93 -0
- zeno_cli-0.3.4/src/zeno_cli/__init__.py +1 -0
- zeno_cli-0.3.4/src/zeno_cli/_hooks/cc_bridge.py +1016 -0
- zeno_cli-0.3.4/src/zeno_cli/doctor.py +535 -0
- zeno_cli-0.3.4/src/zeno_cli/hook_install.py +269 -0
- zeno_cli-0.3.4/src/zeno_cli/hud/__init__.py +1 -0
- zeno_cli-0.3.4/src/zeno_cli/hud/hud_install.py +652 -0
- zeno_cli-0.3.4/src/zeno_cli/hud/zeno_attention.py +288 -0
- zeno_cli-0.3.4/src/zeno_cli/hud/zeno_cognition.py +457 -0
- zeno_cli-0.3.4/src/zeno_cli/hud/zeno_hud.py +496 -0
- zeno_cli-0.3.4/src/zeno_cli/interview_invites.py +342 -0
- zeno_cli-0.3.4/src/zeno_cli/login.py +241 -0
- zeno_cli-0.3.4/src/zeno_cli/main.py +2534 -0
- zeno_cli-0.3.4/src/zeno_cli/onboard.py +206 -0
- zeno_cli-0.3.4/src/zeno_cli/outreach.py +456 -0
- zeno_cli-0.3.4/src/zeno_cli/version.py +67 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.venv/
|
|
7
|
+
.coverage
|
|
8
|
+
.coverage.*
|
|
9
|
+
htmlcov/
|
|
10
|
+
|
|
11
|
+
# zeno-cli build-time vendor stage (apps/cli/hatch_build.py writes here)
|
|
12
|
+
_vendor_build/
|
|
13
|
+
_vendor_build.tmp/
|
|
14
|
+
|
|
15
|
+
# Node
|
|
16
|
+
node_modules/
|
|
17
|
+
.pnpm-store/
|
|
18
|
+
dist/
|
|
19
|
+
coverage/
|
|
20
|
+
.turbo/
|
|
21
|
+
.next/
|
|
22
|
+
*.tsbuildinfo
|
|
23
|
+
|
|
24
|
+
# OS / editor
|
|
25
|
+
.DS_Store
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
|
|
29
|
+
# Env / secrets
|
|
30
|
+
.env
|
|
31
|
+
.env.*
|
|
32
|
+
!.env.example
|
|
33
|
+
|
|
34
|
+
# Local app state
|
|
35
|
+
.zeno/
|
|
36
|
+
|
|
37
|
+
# Local SQLite (dev / demo capture)
|
|
38
|
+
*.db
|
|
39
|
+
*.db-journal
|
|
40
|
+
*.db-wal
|
|
41
|
+
*.db-shm
|
|
42
|
+
|
|
43
|
+
# Website (apps/website) artifacts
|
|
44
|
+
apps/website/playwright-report/
|
|
45
|
+
apps/website/test-results/
|
|
46
|
+
apps/website/.lighthouseci/
|
zeno_cli-0.3.4/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zeno-cli
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Zeno CLI - measure the supervision cost of AI-assisted work
|
|
5
|
+
Project-URL: Homepage, https://zeno.center
|
|
6
|
+
Project-URL: Repository, https://github.com/Marwan01/zeno
|
|
7
|
+
Author-email: Mar Helali <mar@1dev.space>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,claude-code,cognitive-load,rtlx-s,session-analytics,supervision
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
18
|
+
Requires-Dist: alembic>=1.13.2
|
|
19
|
+
Requires-Dist: keyring>=25.3.0
|
|
20
|
+
Requires-Dist: matplotlib>=3.9.2
|
|
21
|
+
Requires-Dist: numpy>=2.0
|
|
22
|
+
Requires-Dist: pydantic>=2.9.0
|
|
23
|
+
Requires-Dist: rich>=13.8.1
|
|
24
|
+
Requires-Dist: scipy>=1.14.1
|
|
25
|
+
Requires-Dist: textual>=0.86.1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# zeno-cli
|
|
29
|
+
|
|
30
|
+
Measure the supervision cost of AI-assisted work, from your terminal. `zeno`
|
|
31
|
+
records RTLX-S load probes + session activity, fits your personal "babysitting
|
|
32
|
+
tax" curve, and surfaces where your optimal number of parallel agents sits.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Install the `zeno` CLI straight from the monorepo over SSH. It is one
|
|
37
|
+
self-contained package: the SDK, core, adapters, session-intelligence engine,
|
|
38
|
+
and SQLite migrations all ship inside it. Requires **Python >= 3.12** and GitHub
|
|
39
|
+
SSH access to the repo.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uv tool install \
|
|
43
|
+
--from "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli" \
|
|
44
|
+
zeno-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`pipx install "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli"`
|
|
48
|
+
works the same way. The git clone runs an in-tree build, so the hatchling build
|
|
49
|
+
hook (`apps/cli/hatch_build.py`) vendors the workspace packages into the wheel.
|
|
50
|
+
**Update** by bumping the tag: re-run with `@cli-v0.3.3` (uv) or
|
|
51
|
+
`pipx install --force ...@<newtag>`.
|
|
52
|
+
|
|
53
|
+
**Why git+ssh and not an index:** TestPyPI is not a durable distribution channel
|
|
54
|
+
and a private index is overkill at team scale. A public PyPI wheel comes only
|
|
55
|
+
when non-repo-access users do. The packaging blocker for it is now resolved - the
|
|
56
|
+
build is sdist-clean (`uv build --sdist` -> a wheel built from that sdist installs
|
|
57
|
+
and runs every verb; see the build-hook note under [Packaging](#packaging-notes)),
|
|
58
|
+
so the remaining gate is purely "do public users exist yet", not a broken build.
|
|
59
|
+
|
|
60
|
+
### Install channels (staged)
|
|
61
|
+
|
|
62
|
+
The distribution channel widens as the audience does. Use the one that matches
|
|
63
|
+
where the project is, not the most convenient:
|
|
64
|
+
|
|
65
|
+
| Stage | Channel | Command | Status |
|
|
66
|
+
| :--- | :--- | :--- | :--- |
|
|
67
|
+
| Now | git+ssh tag | `uv tool install --from "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli" zeno-cli` | Live (needs repo SSH access) |
|
|
68
|
+
| Next | PyPI | `uv tool install zeno-cli` (or `pipx install zeno-cli`) | Lands when `.github/workflows/publish.yml` runs on the first GitHub release; the packaging metadata is ready |
|
|
69
|
+
| Later | Homebrew | `brew install zeno-cli` | Formula not written yet |
|
|
70
|
+
|
|
71
|
+
The PyPI publish is OIDC Trusted-Publishing gated and fires only on a published
|
|
72
|
+
GitHub release - see `.github/workflows/publish.yml`. Until that first release,
|
|
73
|
+
the git+ssh tag is the supported path.
|
|
74
|
+
|
|
75
|
+
**From a built wheel** (offline / air-gapped; the wheel is fully self-contained):
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
uv build --wheel --package zeno-cli # -> dist/zeno_cli-*.whl
|
|
79
|
+
uv tool install ./dist/zeno_cli-0.3.3-py3-none-any.whl
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**From source** (development inside the monorepo, no install):
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
uv run --project /path/to/zeno zeno <cmd>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
New here? Follow `docs/ADOPT.md` for the 10-minute install -> capture -> survey loop.
|
|
89
|
+
|
|
90
|
+
## Use
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
zeno survey # log how a session felt (~6s, RTLX-S 5-item probe)
|
|
94
|
+
zeno status # tier, streak, tax curve, cognitive state, today's survey
|
|
95
|
+
zeno curve # render the babysitting-tax curve (needs ~10 sessions of data)
|
|
96
|
+
zeno weekly # 7-day rollup
|
|
97
|
+
zeno doctor # health check: API reachability + identity
|
|
98
|
+
zeno login # browser sign-in; stores a Clerk JWT in the OS keyring
|
|
99
|
+
zeno whoami # show the identity + tier the API sees for your token
|
|
100
|
+
zeno logout # clear the stored token
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Authentication
|
|
104
|
+
|
|
105
|
+
`zeno login` runs a browser + loopback sign-in and stores the resulting Clerk
|
|
106
|
+
JWT in your OS keyring; the CLI sends it as a `Bearer` token so the API
|
|
107
|
+
attributes requests to your user. Token resolution order, used by every
|
|
108
|
+
command: `ZENO_API_TOKEN` env override first, then the keyring token from
|
|
109
|
+
`zeno login`, then none (local-only). Use `zeno login --no-browser` for SSH /
|
|
110
|
+
headless (prints the URL), and `--authorize-url` / `ZENO_LOGIN_AUTHORIZE_URL`
|
|
111
|
+
to target a non-default bridge.
|
|
112
|
+
|
|
113
|
+
> The dashboard `/cli/authorize` bridge page + the Clerk `zeno-api` JWT template
|
|
114
|
+
> that mint the token are a documented follow-up (not yet shipped). See
|
|
115
|
+
> `src/zeno_cli/login.py` for the callback contract.
|
|
116
|
+
|
|
117
|
+
Full daily-loop guide: `docs/CLI_QUICKSTART.md`. Requires Python >= 3.12.
|
|
118
|
+
|
|
119
|
+
## Cognition HUD add-on
|
|
120
|
+
|
|
121
|
+
`zeno-hud-bar` is the single differentiated cognition line (`att / eff / drv`) that
|
|
122
|
+
stacks UNDER whatever popular HUD you already run, instead of replacing it. Claude Code
|
|
123
|
+
has one statusLine slot, so zeno rides the HUD you have:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
zeno hud install # auto-detect: ccstatusline (native widget) or claude-hud (wrapper)
|
|
127
|
+
zeno hud install --dry-run # print what would change, write nothing
|
|
128
|
+
zeno hud status # what is detected + whether the line is wired in
|
|
129
|
+
zeno hud uninstall # remove from both adapters (idempotent; --restore for byte-for-byte)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`--target {auto,ccstatusline,claude-hud}` forces an adapter (auto prefers ccstatusline
|
|
133
|
+
when both are present). Every edit is backed up and reversible. The bar is crash-safe: on
|
|
134
|
+
any error it prints an empty line and exits 0, so it never breaks the host HUD. Full model
|
|
135
|
+
and both recipes: `docs/HUD_ADDON.md`.
|
|
136
|
+
|
|
137
|
+
## Packaging notes
|
|
138
|
+
|
|
139
|
+
- **sdist-clean via a build hook.** `apps/cli/hatch_build.py` (a custom hatchling
|
|
140
|
+
build hook, enabled on BOTH the `wheel` and `sdist` targets) vendors the four
|
|
141
|
+
workspace packages (`zeno_core` / `zeno_sdk` / `zeno_adapters` /
|
|
142
|
+
`zeno_session_intel`) plus the SDK alembic tree into an in-root, gitignored
|
|
143
|
+
`_vendor_build/` stage at build time. The wheel target flattens them to the
|
|
144
|
+
archive top level (the exact layout the old static `force-include` produced, so
|
|
145
|
+
absolute imports resolve unchanged); the sdist target ships the whole
|
|
146
|
+
`_vendor_build/` stage inside the tarball so a wheel built FROM the sdist finds
|
|
147
|
+
the sources locally. This replaced the static `[tool.hatch...force-include]` of
|
|
148
|
+
`../../packages/...` relative-parent paths, whose `..` escaped the sdist root and
|
|
149
|
+
were silently dropped - a wheel from that sdist died with "Forced include not
|
|
150
|
+
found". Repo source under `packages/*` is never moved, so the uv workspace, the
|
|
151
|
+
API, and the test suite keep importing the editable members unchanged; the
|
|
152
|
+
vendored copy is purely a build artifact.
|
|
153
|
+
- `zeno ingest` / `zeno usage` are backed by `zeno_session_intel`, vendored
|
|
154
|
+
alongside the other internal packages by the build hook. It is stdlib-only with
|
|
155
|
+
relative-only internal imports, so it flattens into the wheel with zero import
|
|
156
|
+
rewrites. Omit it and both subcommands abort with a "not bundled" hint at runtime
|
|
157
|
+
(the regression fixed in 0.3.3); `scripts/install-smoke.sh` now exercises every
|
|
158
|
+
advertised subcommand so this cannot ship silently again.
|
|
159
|
+
- Agent-framework adapters (`zeno_sdk.adapters` -> crewai / openai / anthropic /
|
|
160
|
+
langgraph) are off the CLI path and pull heavy framework deps; they are kept
|
|
161
|
+
optional and will be exposed as extras (e.g. `zeno-cli[crewai]`) in a later stage.
|
zeno_cli-0.3.4/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# zeno-cli
|
|
2
|
+
|
|
3
|
+
Measure the supervision cost of AI-assisted work, from your terminal. `zeno`
|
|
4
|
+
records RTLX-S load probes + session activity, fits your personal "babysitting
|
|
5
|
+
tax" curve, and surfaces where your optimal number of parallel agents sits.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install the `zeno` CLI straight from the monorepo over SSH. It is one
|
|
10
|
+
self-contained package: the SDK, core, adapters, session-intelligence engine,
|
|
11
|
+
and SQLite migrations all ship inside it. Requires **Python >= 3.12** and GitHub
|
|
12
|
+
SSH access to the repo.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
uv tool install \
|
|
16
|
+
--from "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli" \
|
|
17
|
+
zeno-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`pipx install "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli"`
|
|
21
|
+
works the same way. The git clone runs an in-tree build, so the hatchling build
|
|
22
|
+
hook (`apps/cli/hatch_build.py`) vendors the workspace packages into the wheel.
|
|
23
|
+
**Update** by bumping the tag: re-run with `@cli-v0.3.3` (uv) or
|
|
24
|
+
`pipx install --force ...@<newtag>`.
|
|
25
|
+
|
|
26
|
+
**Why git+ssh and not an index:** TestPyPI is not a durable distribution channel
|
|
27
|
+
and a private index is overkill at team scale. A public PyPI wheel comes only
|
|
28
|
+
when non-repo-access users do. The packaging blocker for it is now resolved - the
|
|
29
|
+
build is sdist-clean (`uv build --sdist` -> a wheel built from that sdist installs
|
|
30
|
+
and runs every verb; see the build-hook note under [Packaging](#packaging-notes)),
|
|
31
|
+
so the remaining gate is purely "do public users exist yet", not a broken build.
|
|
32
|
+
|
|
33
|
+
### Install channels (staged)
|
|
34
|
+
|
|
35
|
+
The distribution channel widens as the audience does. Use the one that matches
|
|
36
|
+
where the project is, not the most convenient:
|
|
37
|
+
|
|
38
|
+
| Stage | Channel | Command | Status |
|
|
39
|
+
| :--- | :--- | :--- | :--- |
|
|
40
|
+
| Now | git+ssh tag | `uv tool install --from "git+ssh://git@github.com/Marwan01/zeno.git@cli-v0.3.3#subdirectory=apps/cli" zeno-cli` | Live (needs repo SSH access) |
|
|
41
|
+
| Next | PyPI | `uv tool install zeno-cli` (or `pipx install zeno-cli`) | Lands when `.github/workflows/publish.yml` runs on the first GitHub release; the packaging metadata is ready |
|
|
42
|
+
| Later | Homebrew | `brew install zeno-cli` | Formula not written yet |
|
|
43
|
+
|
|
44
|
+
The PyPI publish is OIDC Trusted-Publishing gated and fires only on a published
|
|
45
|
+
GitHub release - see `.github/workflows/publish.yml`. Until that first release,
|
|
46
|
+
the git+ssh tag is the supported path.
|
|
47
|
+
|
|
48
|
+
**From a built wheel** (offline / air-gapped; the wheel is fully self-contained):
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv build --wheel --package zeno-cli # -> dist/zeno_cli-*.whl
|
|
52
|
+
uv tool install ./dist/zeno_cli-0.3.3-py3-none-any.whl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**From source** (development inside the monorepo, no install):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uv run --project /path/to/zeno zeno <cmd>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
New here? Follow `docs/ADOPT.md` for the 10-minute install -> capture -> survey loop.
|
|
62
|
+
|
|
63
|
+
## Use
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
zeno survey # log how a session felt (~6s, RTLX-S 5-item probe)
|
|
67
|
+
zeno status # tier, streak, tax curve, cognitive state, today's survey
|
|
68
|
+
zeno curve # render the babysitting-tax curve (needs ~10 sessions of data)
|
|
69
|
+
zeno weekly # 7-day rollup
|
|
70
|
+
zeno doctor # health check: API reachability + identity
|
|
71
|
+
zeno login # browser sign-in; stores a Clerk JWT in the OS keyring
|
|
72
|
+
zeno whoami # show the identity + tier the API sees for your token
|
|
73
|
+
zeno logout # clear the stored token
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Authentication
|
|
77
|
+
|
|
78
|
+
`zeno login` runs a browser + loopback sign-in and stores the resulting Clerk
|
|
79
|
+
JWT in your OS keyring; the CLI sends it as a `Bearer` token so the API
|
|
80
|
+
attributes requests to your user. Token resolution order, used by every
|
|
81
|
+
command: `ZENO_API_TOKEN` env override first, then the keyring token from
|
|
82
|
+
`zeno login`, then none (local-only). Use `zeno login --no-browser` for SSH /
|
|
83
|
+
headless (prints the URL), and `--authorize-url` / `ZENO_LOGIN_AUTHORIZE_URL`
|
|
84
|
+
to target a non-default bridge.
|
|
85
|
+
|
|
86
|
+
> The dashboard `/cli/authorize` bridge page + the Clerk `zeno-api` JWT template
|
|
87
|
+
> that mint the token are a documented follow-up (not yet shipped). See
|
|
88
|
+
> `src/zeno_cli/login.py` for the callback contract.
|
|
89
|
+
|
|
90
|
+
Full daily-loop guide: `docs/CLI_QUICKSTART.md`. Requires Python >= 3.12.
|
|
91
|
+
|
|
92
|
+
## Cognition HUD add-on
|
|
93
|
+
|
|
94
|
+
`zeno-hud-bar` is the single differentiated cognition line (`att / eff / drv`) that
|
|
95
|
+
stacks UNDER whatever popular HUD you already run, instead of replacing it. Claude Code
|
|
96
|
+
has one statusLine slot, so zeno rides the HUD you have:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
zeno hud install # auto-detect: ccstatusline (native widget) or claude-hud (wrapper)
|
|
100
|
+
zeno hud install --dry-run # print what would change, write nothing
|
|
101
|
+
zeno hud status # what is detected + whether the line is wired in
|
|
102
|
+
zeno hud uninstall # remove from both adapters (idempotent; --restore for byte-for-byte)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`--target {auto,ccstatusline,claude-hud}` forces an adapter (auto prefers ccstatusline
|
|
106
|
+
when both are present). Every edit is backed up and reversible. The bar is crash-safe: on
|
|
107
|
+
any error it prints an empty line and exits 0, so it never breaks the host HUD. Full model
|
|
108
|
+
and both recipes: `docs/HUD_ADDON.md`.
|
|
109
|
+
|
|
110
|
+
## Packaging notes
|
|
111
|
+
|
|
112
|
+
- **sdist-clean via a build hook.** `apps/cli/hatch_build.py` (a custom hatchling
|
|
113
|
+
build hook, enabled on BOTH the `wheel` and `sdist` targets) vendors the four
|
|
114
|
+
workspace packages (`zeno_core` / `zeno_sdk` / `zeno_adapters` /
|
|
115
|
+
`zeno_session_intel`) plus the SDK alembic tree into an in-root, gitignored
|
|
116
|
+
`_vendor_build/` stage at build time. The wheel target flattens them to the
|
|
117
|
+
archive top level (the exact layout the old static `force-include` produced, so
|
|
118
|
+
absolute imports resolve unchanged); the sdist target ships the whole
|
|
119
|
+
`_vendor_build/` stage inside the tarball so a wheel built FROM the sdist finds
|
|
120
|
+
the sources locally. This replaced the static `[tool.hatch...force-include]` of
|
|
121
|
+
`../../packages/...` relative-parent paths, whose `..` escaped the sdist root and
|
|
122
|
+
were silently dropped - a wheel from that sdist died with "Forced include not
|
|
123
|
+
found". Repo source under `packages/*` is never moved, so the uv workspace, the
|
|
124
|
+
API, and the test suite keep importing the editable members unchanged; the
|
|
125
|
+
vendored copy is purely a build artifact.
|
|
126
|
+
- `zeno ingest` / `zeno usage` are backed by `zeno_session_intel`, vendored
|
|
127
|
+
alongside the other internal packages by the build hook. It is stdlib-only with
|
|
128
|
+
relative-only internal imports, so it flattens into the wheel with zero import
|
|
129
|
+
rewrites. Omit it and both subcommands abort with a "not bundled" hint at runtime
|
|
130
|
+
(the regression fixed in 0.3.3); `scripts/install-smoke.sh` now exercises every
|
|
131
|
+
advertised subcommand so this cannot ship silently again.
|
|
132
|
+
- Agent-framework adapters (`zeno_sdk.adapters` -> crewai / openai / anthropic /
|
|
133
|
+
langgraph) are off the CLI path and pull heavy framework deps; they are kept
|
|
134
|
+
optional and will be exposed as extras (e.g. `zeno-cli[crewai]`) in a later stage.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Adapter package exports."""
|
|
2
|
+
|
|
3
|
+
from .anthropic import instrument as instrument_anthropic
|
|
4
|
+
from .claude_code import instrument as instrument_claude_code
|
|
5
|
+
from .crewai import instrument_agent as instrument_crewai_agent
|
|
6
|
+
from .crewai import instrument_crew as instrument_crewai_crew
|
|
7
|
+
from .langgraph import instrument as instrument_langgraph
|
|
8
|
+
from .openai import instrument as instrument_openai
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"instrument_anthropic",
|
|
12
|
+
"instrument_claude_code",
|
|
13
|
+
"instrument_crewai_agent",
|
|
14
|
+
"instrument_crewai_crew",
|
|
15
|
+
"instrument_langgraph",
|
|
16
|
+
"instrument_openai",
|
|
17
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get(obj: Any, key: str, default: Any = None) -> Any:
|
|
9
|
+
if isinstance(obj, dict):
|
|
10
|
+
return obj.get(key, default)
|
|
11
|
+
return getattr(obj, key, default)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _tool_names_from_anthropic(result: Any) -> list[str]:
|
|
15
|
+
content = _get(result, "content", [])
|
|
16
|
+
names: list[str] = []
|
|
17
|
+
for item in content or []:
|
|
18
|
+
if _get(item, "type") == "tool_use":
|
|
19
|
+
name = _get(item, "name")
|
|
20
|
+
if isinstance(name, str):
|
|
21
|
+
names.append(name)
|
|
22
|
+
return names
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract_usage(result: Any) -> tuple[int | None, int | None]:
|
|
26
|
+
usage = _get(result, "usage")
|
|
27
|
+
if usage is None:
|
|
28
|
+
return (None, None)
|
|
29
|
+
input_tokens = _get(usage, "input_tokens")
|
|
30
|
+
output_tokens = _get(usage, "output_tokens")
|
|
31
|
+
return (input_tokens, output_tokens)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def call_with_timing(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> tuple[Any, int]:
|
|
35
|
+
started = time.perf_counter_ns()
|
|
36
|
+
result = fn(*args, **kwargs)
|
|
37
|
+
latency_ms = int((time.perf_counter_ns() - started) / 1_000_000)
|
|
38
|
+
return result, max(latency_ms, 0)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._common import _extract_usage, _tool_names_from_anthropic, call_with_timing
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class _MessagesProxy:
|
|
11
|
+
inner: Any
|
|
12
|
+
zeno_session: Any
|
|
13
|
+
record_content: bool = False
|
|
14
|
+
|
|
15
|
+
def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
16
|
+
model = kwargs.get("model", "unknown")
|
|
17
|
+
with self.zeno_session.agent_run(model=model) as run:
|
|
18
|
+
try:
|
|
19
|
+
result, latency_ms = call_with_timing(self.inner.create, *args, **kwargs)
|
|
20
|
+
input_tokens, output_tokens = _extract_usage(result)
|
|
21
|
+
metadata: dict[str, Any] = {
|
|
22
|
+
"provider": "anthropic",
|
|
23
|
+
"harness": "anthropic",
|
|
24
|
+
"input_tokens": input_tokens,
|
|
25
|
+
"output_tokens": output_tokens,
|
|
26
|
+
"tool_names": _tool_names_from_anthropic(result),
|
|
27
|
+
"tool_call_count": len(_tool_names_from_anthropic(result)),
|
|
28
|
+
}
|
|
29
|
+
if self.record_content:
|
|
30
|
+
metadata["prompt"] = str(kwargs.get("messages"))
|
|
31
|
+
metadata["output"] = str(getattr(result, "content", None))
|
|
32
|
+
|
|
33
|
+
self.zeno_session.record(
|
|
34
|
+
type="accept",
|
|
35
|
+
latency_ms_to_decide=latency_ms,
|
|
36
|
+
metadata=metadata,
|
|
37
|
+
)
|
|
38
|
+
run.outcome("accepted")
|
|
39
|
+
return result
|
|
40
|
+
except Exception:
|
|
41
|
+
self.zeno_session.record(
|
|
42
|
+
type="reject",
|
|
43
|
+
metadata={"provider": "anthropic", "harness": "anthropic"},
|
|
44
|
+
)
|
|
45
|
+
run.outcome("rejected")
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class _ClientProxy:
|
|
51
|
+
client: Any
|
|
52
|
+
zeno_session: Any
|
|
53
|
+
record_content: bool = False
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def messages(self) -> _MessagesProxy:
|
|
57
|
+
return _MessagesProxy(
|
|
58
|
+
inner=self.client.messages,
|
|
59
|
+
zeno_session=self.zeno_session,
|
|
60
|
+
record_content=self.record_content,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, item: str) -> Any:
|
|
64
|
+
return getattr(self.client, item)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def instrument(client: Any, *, zeno_session: Any, record_content: bool = False) -> Any:
|
|
68
|
+
return _ClientProxy(client=client, zeno_session=zeno_session, record_content=record_content)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._common import _extract_usage, _tool_names_from_anthropic, call_with_timing
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _trace_metadata(tracer: Any) -> dict[str, Any]:
|
|
10
|
+
if tracer is None:
|
|
11
|
+
return {"trace_event_count": 0, "trace_event_types": []}
|
|
12
|
+
events = tracer.get_recent_events()
|
|
13
|
+
event_types = sorted(
|
|
14
|
+
{
|
|
15
|
+
event.get("type")
|
|
16
|
+
for event in events
|
|
17
|
+
if isinstance(event, dict) and isinstance(event.get("type"), str)
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
return {
|
|
21
|
+
"trace_event_count": len(events),
|
|
22
|
+
"trace_event_types": event_types,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class _MessagesProxy:
|
|
28
|
+
inner: Any
|
|
29
|
+
tracer: Any
|
|
30
|
+
zeno_session: Any
|
|
31
|
+
record_content: bool = False
|
|
32
|
+
|
|
33
|
+
def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
34
|
+
model = kwargs.get("model", "unknown")
|
|
35
|
+
with self.zeno_session.agent_run(model=model) as run:
|
|
36
|
+
try:
|
|
37
|
+
result, latency_ms = call_with_timing(self.inner.create, *args, **kwargs)
|
|
38
|
+
input_tokens, output_tokens = _extract_usage(result)
|
|
39
|
+
tool_names = _tool_names_from_anthropic(result)
|
|
40
|
+
metadata: dict[str, Any] = {
|
|
41
|
+
"provider": "anthropic",
|
|
42
|
+
"harness": "claude-code",
|
|
43
|
+
"input_tokens": input_tokens,
|
|
44
|
+
"output_tokens": output_tokens,
|
|
45
|
+
"tool_names": tool_names,
|
|
46
|
+
"tool_call_count": len(tool_names),
|
|
47
|
+
}
|
|
48
|
+
metadata.update(_trace_metadata(self.tracer))
|
|
49
|
+
if self.record_content:
|
|
50
|
+
metadata["prompt"] = str(kwargs.get("messages"))
|
|
51
|
+
metadata["output"] = str(getattr(result, "content", None))
|
|
52
|
+
|
|
53
|
+
self.zeno_session.record(
|
|
54
|
+
type="accept",
|
|
55
|
+
latency_ms_to_decide=latency_ms,
|
|
56
|
+
metadata=metadata,
|
|
57
|
+
)
|
|
58
|
+
run.outcome("accepted")
|
|
59
|
+
return result
|
|
60
|
+
except Exception:
|
|
61
|
+
self.zeno_session.record(
|
|
62
|
+
type="reject",
|
|
63
|
+
metadata={"provider": "anthropic", "harness": "claude-code"},
|
|
64
|
+
)
|
|
65
|
+
run.outcome("rejected")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class _ClientProxy:
|
|
71
|
+
client: Any
|
|
72
|
+
tracer: Any
|
|
73
|
+
zeno_session: Any
|
|
74
|
+
record_content: bool = False
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def messages(self) -> _MessagesProxy:
|
|
78
|
+
return _MessagesProxy(
|
|
79
|
+
inner=self.client.messages,
|
|
80
|
+
tracer=self.tracer,
|
|
81
|
+
zeno_session=self.zeno_session,
|
|
82
|
+
record_content=self.record_content,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __getattr__(self, item: str) -> Any:
|
|
86
|
+
return getattr(self.client, item)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def instrument(
|
|
90
|
+
client: Any,
|
|
91
|
+
*,
|
|
92
|
+
tracer: Any,
|
|
93
|
+
zeno_session: Any,
|
|
94
|
+
record_content: bool = False,
|
|
95
|
+
) -> Any:
|
|
96
|
+
return _ClientProxy(
|
|
97
|
+
client=client,
|
|
98
|
+
tracer=tracer,
|
|
99
|
+
zeno_session=zeno_session,
|
|
100
|
+
record_content=record_content,
|
|
101
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._common import call_with_timing
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class InstrumentedAgent:
|
|
11
|
+
inner: Any
|
|
12
|
+
zeno_session: Any
|
|
13
|
+
model: str = "crewai-agent"
|
|
14
|
+
record_content: bool = False
|
|
15
|
+
|
|
16
|
+
def run(self, *args: Any, **kwargs: Any) -> Any:
|
|
17
|
+
with self.zeno_session.agent_run(model=self.model) as run:
|
|
18
|
+
try:
|
|
19
|
+
result, latency_ms = call_with_timing(self.inner.run, *args, **kwargs)
|
|
20
|
+
metadata: dict[str, Any] = {
|
|
21
|
+
"provider": "crewai",
|
|
22
|
+
"harness": "crewai",
|
|
23
|
+
"tool_call_count": int(getattr(result, "tool_call_count", 0)),
|
|
24
|
+
"tool_names": list(getattr(result, "tool_names", [])),
|
|
25
|
+
"status": getattr(result, "status", "success"),
|
|
26
|
+
}
|
|
27
|
+
if self.record_content:
|
|
28
|
+
metadata["prompt"] = str(kwargs.get("task") or kwargs.get("input"))
|
|
29
|
+
metadata["output"] = str(getattr(result, "output", None))
|
|
30
|
+
self.zeno_session.record(
|
|
31
|
+
type="accept",
|
|
32
|
+
latency_ms_to_decide=latency_ms,
|
|
33
|
+
metadata=metadata,
|
|
34
|
+
)
|
|
35
|
+
run.outcome("accepted")
|
|
36
|
+
return result
|
|
37
|
+
except Exception:
|
|
38
|
+
self.zeno_session.record(
|
|
39
|
+
type="reject",
|
|
40
|
+
metadata={"provider": "crewai", "harness": "crewai"},
|
|
41
|
+
)
|
|
42
|
+
run.outcome("rejected")
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
def __getattr__(self, item: str) -> Any:
|
|
46
|
+
return getattr(self.inner, item)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class InstrumentedCrew:
|
|
51
|
+
inner: Any
|
|
52
|
+
zeno_session: Any
|
|
53
|
+
model: str = "crewai-crew"
|
|
54
|
+
record_content: bool = False
|
|
55
|
+
|
|
56
|
+
def kickoff(self, *args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
with self.zeno_session.agent_run(model=self.model) as run:
|
|
58
|
+
try:
|
|
59
|
+
result, latency_ms = call_with_timing(self.inner.kickoff, *args, **kwargs)
|
|
60
|
+
metadata: dict[str, Any] = {
|
|
61
|
+
"provider": "crewai",
|
|
62
|
+
"harness": "crewai",
|
|
63
|
+
"status": "kickoff",
|
|
64
|
+
}
|
|
65
|
+
if self.record_content:
|
|
66
|
+
metadata["prompt"] = str(kwargs.get("inputs"))
|
|
67
|
+
metadata["output"] = str(result)
|
|
68
|
+
self.zeno_session.record(
|
|
69
|
+
type="accept",
|
|
70
|
+
latency_ms_to_decide=latency_ms,
|
|
71
|
+
metadata=metadata,
|
|
72
|
+
)
|
|
73
|
+
run.outcome("accepted")
|
|
74
|
+
return result
|
|
75
|
+
except Exception:
|
|
76
|
+
self.zeno_session.record(
|
|
77
|
+
type="reject",
|
|
78
|
+
metadata={"provider": "crewai", "harness": "crewai"},
|
|
79
|
+
)
|
|
80
|
+
run.outcome("rejected")
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, item: str) -> Any:
|
|
84
|
+
return getattr(self.inner, item)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def instrument_agent(agent: Any, *, zeno_session: Any, record_content: bool = False) -> Any:
|
|
88
|
+
return InstrumentedAgent(inner=agent, zeno_session=zeno_session, record_content=record_content)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def instrument_crew(crew: Any, *, zeno_session: Any, record_content: bool = False) -> Any:
|
|
92
|
+
return InstrumentedCrew(inner=crew, zeno_session=zeno_session, record_content=record_content)
|