agenteye 0.1.0b1__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.
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: agenteye
3
+ Version: 0.1.0b1
4
+ Summary: Command-line client for the AgentEye dashboard API
5
+ Author-email: Exosphere <support@exosphere.host>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://exosphere.host
8
+ Project-URL: Documentation, https://github.com/agenteye-enterprise/releases
9
+ Keywords: agents,observability,ai,monitoring,agenteye,cli
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: typer>=0.12
22
+ Requires-Dist: rich>=13
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == "dev"
25
+ Requires-Dist: respx>=0.21; extra == "dev"
26
+
27
+ # AgentEye CLI (`agenteye`)
28
+
29
+ A command-line client for the AgentEye **dashboard** API. It lets a developer —
30
+ or the coding agent working alongside them — authenticate and query agent
31
+ sessions, event logs, and evaluations from the terminal, with a `--json` flag on
32
+ every command for scripting.
33
+
34
+ > This is the **`agenteye` CLI**, distinct from the collector daemon
35
+ > (`agenteye-collector`). The PyPI package and the installed command are both
36
+ > `agenteye`.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pipx install agenteye # recommended (isolated)
42
+ # or: uv tool install agenteye / pip install agenteye
43
+ ```
44
+
45
+ Install isolated (pipx / uv tool) — the AgentEye Python SDK shares the `agenteye`
46
+ distribution name, so isolation avoids a clash in a shared virtualenv.
47
+
48
+ For development in this repo:
49
+
50
+ ```bash
51
+ cd cli
52
+ uv sync --extra dev
53
+ uv run agenteye --help
54
+ ```
55
+
56
+ ## Authentication
57
+
58
+ The CLI talks to the dashboard (default `http://localhost:3000`) and logs in with
59
+ an emailed one-time code:
60
+
61
+ ```bash
62
+ agenteye login --email you@example.com
63
+ # enter the 6-digit code; the session is stored in ~/.agenteye/cli.json (mode 0600)
64
+ agenteye whoami
65
+ agenteye logout
66
+ ```
67
+
68
+ Sessions expire (24h by default); re-run `agenteye login` when prompted.
69
+
70
+ ## Commands
71
+
72
+ ```text
73
+ agenteye sessions [--since 24h] [--status error] [--all]
74
+ agenteye events --session-id <id> [--event-type tool_use,tool_result] [--all]
75
+ agenteye logs ... # alias of events
76
+ agenteye evals --score helpfulness:0.5..0.8 --latest-per-session
77
+ agenteye session show <id>
78
+ agenteye session export <id> -o out.json
79
+ agenteye re-evaluate <id>
80
+ agenteye jobs # in-flight evaluation queue
81
+ agenteye environments [--source events|evals]
82
+ agenteye version # print the CLI version
83
+ agenteye help # show top-level help
84
+ ```
85
+
86
+ Add `--json` to any command for machine-readable output:
87
+
88
+ ```bash
89
+ agenteye events --session-id run-001 --all --json | jq '.events[].payload'
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ | Setting | Flag | Env var | Default |
95
+ |---|---|---|---|
96
+ | Dashboard URL | `--base-url` | `AGENTEYE_DASHBOARD_URL` | `http://localhost:3000` |
97
+ | Session token | `--token` | `AGENTEYE_CLI_TOKEN` | from `~/.agenteye/cli.json` |
98
+ | JSON output | `--json` | `AGENTEYE_CLI_JSON` | off |
99
+
100
+ Precedence is **flag > environment variable > config file > default**. The config
101
+ directory honours `AGENTEYE_HOME` (same as the SDK and collector).
102
+
103
+ ## Exit codes
104
+
105
+ | Code | Meaning |
106
+ |---|---|
107
+ | 0 | Success |
108
+ | 2 | Usage error (bad arguments) |
109
+ | 3 | Cannot reach the dashboard |
110
+ | 4 | Not logged in / session expired |
111
+ | 5 | Authenticated but missing permission |
112
+
113
+ ## Tests
114
+
115
+ ```bash
116
+ cd cli
117
+ uv run --extra dev pytest
118
+ ```
@@ -0,0 +1,24 @@
1
+ agenteye_cli/__init__.py,sha256=eyyZX0uQp1piaMqpjvK32dLQtbmYb1LDWOhjJQlg70Y,377
2
+ agenteye_cli/__main__.py,sha256=GhA_jQK0HPEAxwgfSYLfQz8t5uDWTBxCY5icQ_MuVrw,59
3
+ agenteye_cli/_context.py,sha256=IC2XnFIx6c7NO5LF3O1th9-_gljDaM_qJkt83QUlGTU,1549
4
+ agenteye_cli/_version.py,sha256=2f9m0YyP0UybST_fqXzfDdE3BUiRQVajDfkzhl26WPw,24
5
+ agenteye_cli/app.py,sha256=7cZ3fcEXv3LCoi3JycM0sNsMrPUnMuTGVYJORqTZU4E,2678
6
+ agenteye_cli/auth.py,sha256=s6zP3LCRgKbP36oo0KJtOoeWg_MRPTych8qobPoeaho,4026
7
+ agenteye_cli/client.py,sha256=ND3jFIha6knxV10pdV5iVBvxpzfd87GpFEdfgdE06vE,10004
8
+ agenteye_cli/config.py,sha256=H9-9iAdhja61LB--wwO9xkMpk87ePXXeeHFzaMRBH0A,2989
9
+ agenteye_cli/dates.py,sha256=wCabqz9AgaGwQs6ucs0e_b0MrL_UhcF7hEP-AZN23gs,1616
10
+ agenteye_cli/errors.py,sha256=yUtXlxtPOIt2P8nZuy78ITWEWMEabDiPO496W0Fx-j8,1481
11
+ agenteye_cli/models.py,sha256=NwUOabAayNPvy4bq0S08Xz48o4gN9Cucwb84twdJD2s,3628
12
+ agenteye_cli/output.py,sha256=R0-W2pny8iSYysFxKLhCPsEnNuSJVExfSzzkYOhspcM,2367
13
+ agenteye_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ agenteye_cli/commands/auth_cmds.py,sha256=XP6KKj1XJnC_cPY3-RDNDXFXUQ_9ohUEbr8kVXyVLAk,2841
15
+ agenteye_cli/commands/env_cmds.py,sha256=fG5Q-zBCHAPTwy8gzfidecvAQQWadHYW2pmKjmLG3Pc,975
16
+ agenteye_cli/commands/evals_cmds.py,sha256=Xkm5ebZ8Et4Ds54Oy0OcbrM_i9SMRqcRWfBu-gh52K4,3244
17
+ agenteye_cli/commands/events_cmds.py,sha256=4ThYr3ypJzJm5r3jMgytJXCOb1T4Lbk1i9_22tf22K0,3105
18
+ agenteye_cli/commands/jobs_cmds.py,sha256=KcE2MTuOa2yL2956xQv-yX0-8pepoq1XF7zQTKfRMig,1128
19
+ agenteye_cli/commands/sessions_cmds.py,sha256=gmAO4_I3TYDp82tlZ9YyapGwysH4afv5fZ7JTJkxYds,5885
20
+ agenteye-0.1.0b1.dist-info/METADATA,sha256=JvOOddMyux2a262MirFm1uYEt8TbZvPoQ1Ka51CkXC4,3662
21
+ agenteye-0.1.0b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ agenteye-0.1.0b1.dist-info/entry_points.txt,sha256=lRieI4-qJsfCnYXUW8piSTVY8s7N5D_zHBUmNAqMB4s,50
23
+ agenteye-0.1.0b1.dist-info/top_level.txt,sha256=oc6HWsjPbGZpd-ctGjZd5lq3_4c7HIBW5kfAdIzkbrE,13
24
+ agenteye-0.1.0b1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agenteye = agenteye_cli.app:app
@@ -0,0 +1 @@
1
+ agenteye_cli
@@ -0,0 +1,10 @@
1
+ """AgentEye CLI — a command-line client for the AgentEye dashboard API.
2
+
3
+ The query layer lives in :mod:`agenteye_cli.client` as pure functions that take a
4
+ ``ClientContext`` and return plain dataclasses. They never print and never import
5
+ Typer/Rich, so a future MCP server can wrap them with zero duplication.
6
+ """
7
+
8
+ from ._version import __version__
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from .app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,50 @@
1
+ """Shared command-layer state and helpers.
2
+
3
+ Kept separate from ``app.py`` so command modules can import these without a
4
+ circular dependency (``app.py`` imports the command modules).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Optional, Tuple
11
+
12
+ import click
13
+
14
+ from . import config as cfgmod
15
+ from . import dates as _dates
16
+ from .client import ClientContext
17
+ from .errors import AuthError
18
+
19
+
20
+ @dataclass
21
+ class AppState:
22
+ json: bool
23
+ base_url: str
24
+ token: Optional[str]
25
+ timeout: float
26
+ config: cfgmod.CliConfig
27
+
28
+
29
+ def build_context(state: AppState) -> ClientContext:
30
+ return ClientContext(base_url=state.base_url, token=state.token, timeout=state.timeout)
31
+
32
+
33
+ def require_auth(state: AppState) -> ClientContext:
34
+ """Return a client context, or raise AuthError if not usably authenticated."""
35
+ if not state.token:
36
+ raise AuthError("Not logged in. Run 'agenteye login'.")
37
+ # Only enforce local expiry when the token came from the stored config; an
38
+ # explicit --token / env override has no known expiry, so trust it.
39
+ if state.token == state.config.session_token and cfgmod.is_expired(state.config):
40
+ raise AuthError("Session expired. Run 'agenteye login'.")
41
+ return build_context(state)
42
+
43
+
44
+ def resolve_dates(
45
+ since: Optional[str], ts_from: Optional[str], ts_to: Optional[str]
46
+ ) -> Tuple[Optional[str], Optional[str]]:
47
+ try:
48
+ return _dates.resolve_range(since, ts_from, ts_to)
49
+ except ValueError as exc:
50
+ raise click.BadParameter(str(exc))
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0b1"
agenteye_cli/app.py ADDED
@@ -0,0 +1,92 @@
1
+ """Typer application: global options + command registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from . import config as cfgmod
10
+ from . import output
11
+ from ._context import AppState
12
+ from ._version import __version__
13
+ from .commands import (
14
+ auth_cmds,
15
+ env_cmds,
16
+ evals_cmds,
17
+ events_cmds,
18
+ jobs_cmds,
19
+ sessions_cmds,
20
+ )
21
+
22
+ app = typer.Typer(
23
+ no_args_is_help=True,
24
+ add_completion=False,
25
+ help="Query the AgentEye dashboard — sessions, events, and evaluations — from your terminal.",
26
+ )
27
+ session_app = typer.Typer(no_args_is_help=True, help="Inspect or export a single session.")
28
+ app.add_typer(session_app, name="session")
29
+
30
+
31
+ def _version_callback(value: bool) -> None:
32
+ if value:
33
+ print(__version__)
34
+ raise typer.Exit()
35
+
36
+
37
+ @app.callback()
38
+ def main(
39
+ ctx: typer.Context,
40
+ json_output: bool = typer.Option(
41
+ False, "--json", envvar="AGENTEYE_CLI_JSON", help="Emit JSON to stdout instead of a table."
42
+ ),
43
+ base_url: Optional[str] = typer.Option(
44
+ None,
45
+ "--base-url",
46
+ envvar="AGENTEYE_DASHBOARD_URL",
47
+ metavar="URL",
48
+ help="Dashboard base URL (default http://localhost:3000).",
49
+ ),
50
+ token: Optional[str] = typer.Option(
51
+ None, "--token", envvar="AGENTEYE_CLI_TOKEN", help="Session token override (for CI/agents)."
52
+ ),
53
+ timeout: float = typer.Option(30.0, "--timeout", help="HTTP timeout in seconds."),
54
+ no_color: bool = typer.Option(
55
+ False, "--no-color", envvar="NO_COLOR", help="Disable coloured output."
56
+ ),
57
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress status messages on stderr."),
58
+ version: bool = typer.Option(
59
+ False, "--version", callback=_version_callback, is_eager=True, help="Show version and exit."
60
+ ),
61
+ ) -> None:
62
+ """Resolve global options into the per-invocation AppState (flag > env > config > default)."""
63
+ cfg = cfgmod.load_config()
64
+ output.configure(no_color=no_color, quiet=quiet)
65
+ ctx.obj = AppState(
66
+ json=json_output,
67
+ base_url=base_url or cfg.base_url or cfgmod.DEFAULT_BASE_URL,
68
+ token=token or cfg.session_token,
69
+ timeout=timeout,
70
+ config=cfg,
71
+ )
72
+
73
+
74
+ @app.command()
75
+ def version() -> None:
76
+ """Show the CLI version."""
77
+ print(__version__)
78
+
79
+
80
+ @app.command("help")
81
+ def help_cmd(ctx: typer.Context) -> None:
82
+ """Show top-level help and available commands."""
83
+ parent = ctx.parent
84
+ typer.echo((parent or ctx).get_help())
85
+
86
+
87
+ auth_cmds.register(app)
88
+ events_cmds.register(app)
89
+ evals_cmds.register(app)
90
+ sessions_cmds.register(app, session_app)
91
+ jobs_cmds.register(app)
92
+ env_cmds.register(app)
agenteye_cli/auth.py ADDED
@@ -0,0 +1,134 @@
1
+ """Email-OTP device login against the dashboard.
2
+
3
+ Flow: ``/api/auth/otp/request`` (sends a code) then ``/api/auth/otp/verify``.
4
+ The verify response carries the session token in the ``ae_session`` **Set-Cookie**
5
+ header (not the JSON body), so we read it from ``response.cookies``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timedelta, timezone
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+ import httpx
14
+
15
+ from .config import CliConfig, save_config
16
+ from .errors import ApiError, AuthError, NetworkError
17
+
18
+
19
+ def _iso(dt: datetime) -> str:
20
+ return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
21
+
22
+
23
+ def _network_error(base_url: str, exc: Exception) -> NetworkError:
24
+ return NetworkError(f"Cannot reach the AgentEye dashboard at {base_url}: {exc}")
25
+
26
+
27
+ def request_otp(
28
+ base_url: str,
29
+ email: str,
30
+ *,
31
+ timeout: float = 30.0,
32
+ transport: Optional[httpx.BaseTransport] = None,
33
+ ) -> None:
34
+ """Ask the dashboard to email a login code. Always succeeds quietly for a
35
+ valid request (the server returns 200 even for unknown emails)."""
36
+ try:
37
+ with httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, transport=transport) as client:
38
+ response = client.post("/api/auth/otp/request", json={"email": email})
39
+ except httpx.RequestError as exc:
40
+ raise _network_error(base_url, exc)
41
+ if response.status_code >= 400:
42
+ raise ApiError(
43
+ f"Failed to request a login code (HTTP {response.status_code}).",
44
+ status=response.status_code,
45
+ )
46
+
47
+
48
+ def verify_otp(
49
+ base_url: str,
50
+ email: str,
51
+ code: str,
52
+ *,
53
+ timeout: float = 30.0,
54
+ transport: Optional[httpx.BaseTransport] = None,
55
+ ) -> Tuple[str, int, Dict[str, Any]]:
56
+ """Exchange the code for a session token. Returns ``(token, expires_in_secs, user)``."""
57
+ try:
58
+ with httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, transport=transport) as client:
59
+ response = client.post(
60
+ "/api/auth/otp/verify", json={"email": email, "code": code}
61
+ )
62
+ except httpx.RequestError as exc:
63
+ raise _network_error(base_url, exc)
64
+
65
+ if response.status_code == 401:
66
+ raise AuthError("Invalid or expired login code.")
67
+ if response.status_code >= 400:
68
+ raise ApiError(
69
+ f"Login verification failed (HTTP {response.status_code}).",
70
+ status=response.status_code,
71
+ )
72
+
73
+ token = response.cookies.get("ae_session")
74
+ if not token:
75
+ raise AuthError("The dashboard did not return a session token.")
76
+
77
+ try:
78
+ body = response.json()
79
+ except Exception:
80
+ body = {}
81
+ if not isinstance(body, dict):
82
+ body = {}
83
+
84
+ try:
85
+ expires_in = int(body.get("expires_in_secs"))
86
+ except (TypeError, ValueError):
87
+ expires_in = 86400
88
+
89
+ user = body.get("user") or {}
90
+ if not isinstance(user, dict):
91
+ user = {}
92
+
93
+ return token, expires_in, user
94
+
95
+
96
+ def persist_session(
97
+ cfg: CliConfig,
98
+ base_url: str,
99
+ token: str,
100
+ expires_in_secs: int,
101
+ user: Dict[str, Any],
102
+ *,
103
+ now: Optional[datetime] = None,
104
+ ) -> CliConfig:
105
+ now = now or datetime.now(timezone.utc)
106
+ cfg.base_url = base_url
107
+ cfg.session_token = token
108
+ cfg.expires_at = _iso(now + timedelta(seconds=expires_in_secs))
109
+ cfg.email = (user or {}).get("email") or cfg.email
110
+ cfg.user_id = (user or {}).get("id") or cfg.user_id
111
+ save_config(cfg)
112
+ return cfg
113
+
114
+
115
+ def logout(
116
+ base_url: str,
117
+ token: Optional[str],
118
+ *,
119
+ timeout: float = 30.0,
120
+ transport: Optional[httpx.BaseTransport] = None,
121
+ ) -> None:
122
+ """Best-effort server-side session revocation; never raises."""
123
+ if not token:
124
+ return
125
+ try:
126
+ with httpx.Client(
127
+ base_url=base_url.rstrip("/"),
128
+ cookies={"ae_session": token},
129
+ timeout=timeout,
130
+ transport=transport,
131
+ ) as client:
132
+ client.post("/api/auth/logout")
133
+ except httpx.RequestError:
134
+ pass