All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 38s
363 lines
10 KiB
Python
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()
|