i forgot too commit
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 38s
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 38s
This commit is contained in:
362
cli/main.py
Normal file
362
cli/main.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user