Files
loyal_companion/cli/main.py
latte d957120eb3
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 38s
i forgot too commit
2026-02-01 15:57:45 +01:00

363 lines
10 KiB
Python

"""Loyal Companion CLI - Main entry point."""
import sys
from pathlib import Path
import typer
from typing_extensions import Annotated
from cli.client import APIError, LoyalCompanionClient
from cli.config import CLIConfig
from cli.formatters import ResponseFormatter
from cli.session import SessionManager
app = typer.Typer(
name="loyal-companion",
help="Loyal Companion CLI - A quiet, terminal-based interface for conversations.",
add_completion=False,
)
def _ensure_authenticated(config: CLIConfig) -> tuple[CLIConfig, str]:
"""Ensure user is authenticated.
Args:
config: CLI configuration
Returns:
tuple: (config, auth_token)
Raises:
typer.Exit: If authentication fails
"""
auth_token = config.get_auth_token()
if not auth_token:
# Need to authenticate
email = config.email
if not email:
email = typer.prompt("Email address")
config.email = email
config.save()
# Request token
try:
client = LoyalCompanionClient(config.get_api_url())
response = client.request_token(email)
auth_token = response.get("token")
if not auth_token:
typer.echo(f"Error: {response.get('message', 'No token received')}", err=True)
raise typer.Exit(1)
# Save token
config.auth_token = auth_token
config.save()
typer.echo(f"Authenticated as {email}")
except APIError as e:
typer.echo(f"Authentication failed: {e}", err=True)
raise typer.Exit(1)
return config, auth_token
@app.command()
def talk(
session_name: Annotated[str, typer.Option("--session", "-s", help="Session name")] = "default",
new: Annotated[bool, typer.Option("--new", "-n", help="Start a new session")] = False,
show_mood: Annotated[
bool, typer.Option("--mood/--no-mood", help="Show mood information")
] = True,
show_relationship: Annotated[
bool, typer.Option("--relationship/--no-relationship", help="Show relationship info")
] = False,
):
"""Start or resume a conversation.
Examples:
lc talk # Resume default session
lc talk --new # Start fresh default session
lc talk -s work # Resume 'work' session
lc talk -s personal --new # Start fresh 'personal' session
"""
# Load config
config = CLIConfig.load()
# Ensure authenticated
config, auth_token = _ensure_authenticated(config)
# Initialize client
client = LoyalCompanionClient(config.get_api_url(), auth_token)
# Initialize session manager
session_manager = SessionManager(config.sessions_file)
# Get or create session
if new:
# Delete old session if exists
session_manager.delete_session(session_name)
session = session_manager.get_or_create_session(session_name)
# Initialize formatter
formatter = ResponseFormatter(
show_mood=show_mood,
show_relationship=show_relationship,
show_facts=config.show_facts,
show_timestamps=config.show_timestamps,
)
# Print welcome message
formatter.print_info("Bartender is here.")
if session.message_count > 0:
formatter.print_info(
f"Resuming session '{session.name}' ({session.message_count} messages)"
)
formatter.print_info("Type your message and press Enter. Press Ctrl+D to end.\n")
# Conversation loop
try:
while True:
# Get user input
try:
user_message = typer.prompt("You", prompt_suffix=": ")
except (EOFError, KeyboardInterrupt):
# User pressed Ctrl+D or Ctrl+C
break
if not user_message.strip():
continue
# Send message
try:
response = client.send_message(session.session_id, user_message)
# Format and display response
formatter.format_response(response)
print() # Empty line for spacing
# Update session
session_manager.update_last_active(session.name)
except APIError as e:
formatter.print_error(str(e))
continue
except KeyboardInterrupt:
pass
# Goodbye message
print() # Empty line
formatter.print_info("Session saved.")
client.close()
@app.command()
def history(
session_name: Annotated[str, typer.Option("--session", "-s", help="Session name")] = "default",
limit: Annotated[int, typer.Option("--limit", "-n", help="Number of messages")] = 50,
):
"""Show conversation history for a session.
Examples:
lc history # Show default session history
lc history -s work # Show 'work' session history
lc history -n 10 # Show last 10 messages
"""
# Load config
config = CLIConfig.load()
# Ensure authenticated
config, auth_token = _ensure_authenticated(config)
# Initialize client
client = LoyalCompanionClient(config.get_api_url(), auth_token)
# Initialize session manager
session_manager = SessionManager(config.sessions_file)
# Get session
session = session_manager.get_session(session_name)
if not session:
typer.echo(f"Session '{session_name}' not found", err=True)
raise typer.Exit(1)
# Get history
try:
response = client.get_history(session.session_id, limit)
messages = response.get("messages", [])
if not messages:
typer.echo("No messages in this session yet.")
raise typer.Exit(0)
# Format and display
formatter = ResponseFormatter(
show_timestamps=True,
use_rich=True,
)
typer.echo(f"History for session '{session.name}' ({len(messages)} messages):\n")
for message in messages:
formatter.format_history_message(message)
print() # Spacing between messages
except APIError as e:
typer.echo(f"Failed to get history: {e}", err=True)
raise typer.Exit(1)
client.close()
@app.command()
def sessions(
delete: Annotated[str | None, typer.Option("--delete", "-d", help="Delete a session")] = None,
):
"""List all sessions or delete a specific session.
Examples:
lc sessions # List all sessions
lc sessions -d work # Delete 'work' session
"""
# Load config
config = CLIConfig.load()
# Initialize session manager
session_manager = SessionManager(config.sessions_file)
if delete:
# Delete session
if session_manager.delete_session(delete):
typer.echo(f"Deleted session '{delete}'")
else:
typer.echo(f"Session '{delete}' not found", err=True)
raise typer.Exit(1)
return
# List sessions
all_sessions = session_manager.list_sessions()
if not all_sessions:
typer.echo("No sessions found.")
return
typer.echo(f"Found {len(all_sessions)} session(s):\n")
for session in all_sessions:
typer.echo(f" {session.name}")
typer.echo(f" Created: {session.created_at}")
typer.echo(f" Last active: {session.last_active}")
typer.echo(f" Messages: {session.message_count}")
typer.echo()
@app.command()
def config_cmd(
show: Annotated[bool, typer.Option("--show", help="Show current configuration")] = False,
set_api_url: Annotated[str | None, typer.Option("--api-url", help="Set API URL")] = None,
set_email: Annotated[str | None, typer.Option("--email", help="Set email")] = None,
reset: Annotated[bool, typer.Option("--reset", help="Reset configuration")] = False,
):
"""Manage CLI configuration.
Examples:
lc config --show # Show current config
lc config --api-url http://localhost:8080 # Set API URL
lc config --email user@example.com # Set email
lc config --reset # Reset to defaults
"""
config = CLIConfig.load()
if reset:
# Delete config file
if config.config_file.exists():
config.config_file.unlink()
typer.echo("Configuration reset to defaults")
return
if set_api_url:
config.api_url = set_api_url
config.save()
typer.echo(f"API URL set to: {set_api_url}")
if set_email:
config.email = set_email
# Clear token when email changes
config.auth_token = None
config.save()
typer.echo(f"Email set to: {set_email}")
typer.echo("(Auth token cleared - you'll need to re-authenticate)")
if show or (not set_api_url and not set_email and not reset):
# Show config
typer.echo("Current configuration:\n")
typer.echo(f" API URL: {config.get_api_url()}")
typer.echo(f" Email: {config.email or '(not set)'}")
typer.echo(f" Authenticated: {'Yes' if config.get_auth_token() else 'No'}")
typer.echo(f" Config file: {config.config_file}")
typer.echo(f" Sessions file: {config.sessions_file}")
@app.command()
def auth(
logout: Annotated[bool, typer.Option("--logout", help="Clear authentication")] = False,
):
"""Manage authentication.
Examples:
lc auth # Show auth status
lc auth --logout # Clear stored token
"""
config = CLIConfig.load()
if logout:
config.auth_token = None
config.save()
typer.echo("Authentication cleared")
return
# Show auth status
if config.get_auth_token():
typer.echo(f"Authenticated as: {config.email}")
else:
typer.echo("Not authenticated")
typer.echo("Run 'lc talk' to authenticate")
@app.command()
def health():
"""Check API health status.
Examples:
lc health # Check if API is reachable
"""
config = CLIConfig.load()
try:
client = LoyalCompanionClient(config.get_api_url())
response = client.health_check()
typer.echo(f"API Status: {response.get('status', 'unknown')}")
typer.echo(f"Platform: {response.get('platform', 'unknown')}")
typer.echo(f"Version: {response.get('version', 'unknown')}")
except APIError as e:
typer.echo(f"Health check failed: {e}", err=True)
raise typer.Exit(1)
def main():
"""Main entry point."""
app()
if __name__ == "__main__":
main()