diff --git a/tests/test_cli.py b/tests/test_cli.py index c2ad51a..8b00b78 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -106,7 +106,9 @@ def test_cli_whoami_command(monkeypatch) -> None: result_json = runner.invoke(cli, ["whoami", "--json"]) assert result_json.exit_code == 0 - assert '"screenName": "testuser"' in result_json.output + payload = yaml.safe_load(runner.invoke(cli, ["whoami", "--yaml"]).output) + assert payload["ok"] is True + assert payload["data"]["user"]["username"] == "testuser" def test_cli_whoami_auto_yaml(monkeypatch) -> None: @@ -122,7 +124,9 @@ def test_cli_whoami_auto_yaml(monkeypatch) -> None: assert result.exit_code == 0 payload = yaml.safe_load(result.output) - assert payload["screenName"] == "testuser" + assert payload["ok"] is True + assert payload["schema_version"] == "1" + assert payload["data"]["user"]["username"] == "testuser" def test_cli_status_auto_yaml(monkeypatch) -> None: @@ -138,8 +142,10 @@ def test_cli_status_auto_yaml(monkeypatch) -> None: assert result.exit_code == 0 payload = yaml.safe_load(result.output) - assert payload["authenticated"] is True - assert payload["user"]["screenName"] == "testuser" + assert payload["ok"] is True + assert payload["schema_version"] == "1" + assert payload["data"]["authenticated"] is True + assert payload["data"]["user"]["username"] == "testuser" def test_cli_reply_command(monkeypatch) -> None: diff --git a/twitter_cli/cli.py b/twitter_cli/cli.py index 9bf2108..9a839a5 100644 --- a/twitter_cli/cli.py +++ b/twitter_cli/cli.py @@ -49,7 +49,15 @@ from .formatter import ( print_user_profile, print_user_table, ) -from .output import default_structured_format, emit_structured, structured_output_options, use_rich_output +from .models import UserProfile +from .output import ( + default_structured_format, + emit_structured, + error_payload, + structured_output_options, + success_payload, + use_rich_output, +) from .serialization import ( tweets_from_json, tweets_to_data, @@ -65,6 +73,27 @@ FEED_TYPES = ["for-you", "following"] SEARCH_PRODUCTS = ["Top", "Latest", "Photos", "Videos"] +def _agent_user_profile(profile: UserProfile) -> dict: + """Normalize a Twitter/X profile for structured agent output.""" + data = user_profile_to_dict(profile) + return { + "id": data["id"], + "name": data["name"], + "username": data["screenName"], + "screenName": data["screenName"], + "bio": data["bio"], + "location": data["location"], + "url": data["url"], + "followers": data["followers"], + "following": data["following"], + "tweets": data["tweets"], + "likes": data["likes"], + "verified": data["verified"], + "profileImageUrl": data["profileImageUrl"], + "createdAt": data["createdAt"], + } + + def _setup_logging(verbose): # type: (bool) -> None level = logging.DEBUG if verbose else logging.WARNING @@ -684,13 +713,13 @@ def status(as_json, as_yaml): client = _get_client_for_output(config, quiet=not rich_output) profile = client.fetch_me() except RuntimeError as exc: - payload = {"authenticated": False, "error": str(exc)} + payload = error_payload("not_authenticated", str(exc)) if emit_structured(payload, as_json=as_json, as_yaml=as_yaml): sys.exit(1) _exit_with_error(exc) return - payload = {"authenticated": True, "user": user_profile_to_dict(profile)} + payload = success_payload({"authenticated": True, "user": _agent_user_profile(profile)}) if emit_structured(payload, as_json=as_json, as_yaml=as_yaml): return @@ -711,9 +740,11 @@ def whoami(as_json, as_yaml): console.print("👤 Fetching current user...") profile = client.fetch_me() except RuntimeError as exc: + if emit_structured(error_payload("not_authenticated", str(exc)), as_json=as_json, as_yaml=as_yaml): + raise SystemExit(1) from None _exit_with_error(exc) - if not emit_structured(user_profile_to_dict(profile), as_json=as_json, as_yaml=as_yaml): + if not emit_structured(success_payload({"user": _agent_user_profile(profile)}), as_json=as_json, as_yaml=as_yaml): console.print() print_user_profile(profile, console) diff --git a/twitter_cli/client.py b/twitter_cli/client.py index 4b96245..308cff3 100644 --- a/twitter_cli/client.py +++ b/twitter_cli/client.py @@ -5,6 +5,7 @@ from __future__ import annotations import json import logging import math +import os import random import re import time diff --git a/twitter_cli/output.py b/twitter_cli/output.py index b31006c..74d6669 100644 --- a/twitter_cli/output.py +++ b/twitter_cli/output.py @@ -11,6 +11,7 @@ import click import yaml _OUTPUT_ENV = "OUTPUT" +_SCHEMA_VERSION = "1" def default_structured_format(*, as_json: bool, as_yaml: bool) -> str | None: @@ -66,3 +67,27 @@ def emit_structured(data: Any, *, as_json: bool, as_yaml: bool) -> bool: ) ) return True + + +def success_payload(data: Any) -> dict[str, Any]: + """Wrap structured success data in the shared agent schema.""" + return { + "ok": True, + "schema_version": _SCHEMA_VERSION, + "data": data, + } + + +def error_payload(code: str, message: str, *, details: Any | None = None) -> dict[str, Any]: + """Wrap structured error data in the shared agent schema.""" + error = { + "code": code, + "message": message, + } + if details is not None: + error["details"] = details + return { + "ok": False, + "schema_version": _SCHEMA_VERSION, + "error": error, + }