"""`agnes admin usage` — telemetry export from the terminal.""" from __future__ import annotations import sys from pathlib import Path from typing import Optional import typer from cli.client import get_client app = typer.Typer(help="Telemetry export and admin queries.") def _handle_error(resp, context: str) -> None: """Print a clean error and exit non-zero for non-2xx responses.""" if resp.status_code in (401, 403): typer.echo( "[err] authentication required — run `agnes auth login` or import a PAT", err=True, ) raise typer.Exit(1) if resp.status_code >= 400: try: body = resp.json().get("detail", resp.text) except Exception: body = resp.text typer.echo(f"[err] server returned {resp.status_code}: {body}", err=True) raise typer.Exit(1) @app.command() def export( format: str = typer.Option("csv", "--format", help="csv|json|parquet"), since: Optional[str] = typer.Option( None, "--since", help="ISO date or datetime, e.g. '2026-01-01' or '2026-05-01T00:00:00Z'." ), until: Optional[str] = typer.Option( None, "--until", help="ISO date or datetime (exclusive upper bound)." ), user: Optional[str] = typer.Option(None, "--user"), source: Optional[str] = typer.Option(None, "--source"), out: Optional[Path] = typer.Option(None, "--out", help="Write to file; else stdout."), ): """Export telemetry events filtered by since/until/user/source.""" if format not in ("csv", "json", "parquet"): typer.echo(f"[err] format must be csv|json|parquet, got {format!r}", err=True) raise typer.Exit(1) params: dict = {"format": format} if since: params["since"] = since if until: params["until"] = until if user: params["user_id"] = user if source: params["source"] = source try: with get_client(timeout=120.0) as client: with client.stream("GET", "/api/admin/telemetry/export", params=params) as resp: if resp.status_code in (401, 403): typer.echo( "[err] authentication required — run `agnes auth login` or import a PAT", err=True, ) raise typer.Exit(1) if resp.status_code >= 400: body = resp.read().decode(errors="replace") typer.echo(f"[err] server returned {resp.status_code}: {body}", err=True) raise typer.Exit(1) sink = out.open("wb") if out else sys.stdout.buffer try: for chunk in resp.iter_bytes(): sink.write(chunk) if out: typer.echo(f"wrote {out}", err=True) finally: if out: sink.close() except typer.Exit: raise except Exception as exc: typer.echo(f"[err] cannot reach server: {exc}", err=True) raise typer.Exit(1) @app.command() def reprocess(): """Force re-extraction of all sessions for the usage processor.""" client = get_client(timeout=60) try: resp = client.post("/api/admin/telemetry/reprocess") except Exception as e: typer.echo(f"[err] cannot reach server: {e}", err=True) raise typer.Exit(1) if resp.status_code == 401: typer.echo("[err] authentication required", err=True) raise typer.Exit(1) if resp.status_code == 403: typer.echo("[err] admin only", err=True) raise typer.Exit(1) if resp.status_code >= 400: typer.echo(f"[err] {resp.status_code}: {resp.text}", err=True) raise typer.Exit(1) data = resp.json() typer.echo("Reprocess scheduled — UsageProcessor will re-extract on next scheduler tick.") for k, v in data.get("deleted", {}).items(): typer.echo(f" deleted {k}: {v}") @app.command() def prune( json_out: bool = typer.Option(False, "--json", help="Emit raw JSON instead of summary."), ): """Prune usage_events older than USAGE_EVENTS_RETENTION_DAYS env var on the server.""" client = get_client(timeout=60) try: resp = client.post("/api/admin/telemetry/prune") except Exception as e: typer.echo(f"[err] cannot reach server: {e}", err=True) raise typer.Exit(1) if resp.status_code == 401: typer.echo("[err] authentication required", err=True) raise typer.Exit(1) if resp.status_code == 403: typer.echo("[err] admin only", err=True) raise typer.Exit(1) if resp.status_code >= 400: typer.echo(f"[err] {resp.status_code}: {resp.text}", err=True) raise typer.Exit(1) data = resp.json() if json_out: import json typer.echo(json.dumps(data, indent=2)) return if data.get("status") == "skipped": typer.echo(f"Skipped: {data.get('reason')}") else: typer.echo( f"Pruned {data['deleted']} events older than {data['retention_days']} days; " f"{data['remaining']} remain." )