mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-01-26 00:52:35 +00:00
restrcuted repo
This commit is contained in:
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
274
cli/cli_interface.py
Normal file
274
cli/cli_interface.py
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI Interface - Simple command line interface for the cycling analyzer
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import sys
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from ..config import Config, load_config, create_sample_config
|
||||
from ..core.core_app import CyclingAnalyzerApp
|
||||
from ..core.template_engine import create_default_templates
|
||||
|
||||
class CLI:
|
||||
"""Command line interface"""
|
||||
|
||||
def __init__(self):
|
||||
self.app = None
|
||||
|
||||
async def run(self, args):
|
||||
"""Main CLI loop"""
|
||||
# Setup configuration
|
||||
try:
|
||||
config = self._setup_config()
|
||||
self.app = CyclingAnalyzerApp(config, test_mode=args.test)
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=getattr(logging, config.log_level.upper()))
|
||||
|
||||
# Initialize app
|
||||
await self.app.initialize()
|
||||
|
||||
if args.command:
|
||||
await self._handle_command(args)
|
||||
else:
|
||||
# Show initial status
|
||||
await self._show_status()
|
||||
# Main loop
|
||||
await self._main_loop()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nGoodbye!")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
logging.error(f"CLI error: {e}", exc_info=True)
|
||||
finally:
|
||||
if self.app:
|
||||
await self.app.cleanup()
|
||||
|
||||
def _setup_config(self) -> Config:
|
||||
"""Setup configuration and default files"""
|
||||
# Create sample config if needed
|
||||
create_sample_config()
|
||||
|
||||
# Load config
|
||||
config = load_config()
|
||||
|
||||
# Validate required settings
|
||||
if not config.openrouter_api_key or config.openrouter_api_key == "your_openrouter_api_key_here":
|
||||
print("Please edit config.yaml with your OpenRouter API key")
|
||||
print("Get your key from: https://openrouter.ai")
|
||||
raise ValueError("OpenRouter API key not configured")
|
||||
|
||||
# Create default templates
|
||||
create_default_templates(config.templates_dir)
|
||||
|
||||
return config
|
||||
|
||||
async def _show_status(self):
|
||||
"""Show application status"""
|
||||
print(f"\nStatus:")
|
||||
print(f"- Available tools: {len(await self.app.list_available_tools())}")
|
||||
print(f"- Available templates: {len(self.app.list_templates())}")
|
||||
print(f"- Cached data keys: {list(self.app.get_cached_data().keys())}")
|
||||
|
||||
async def _main_loop(self):
|
||||
"""Main interaction loop"""
|
||||
while True:
|
||||
print(f"\n{'='*60}")
|
||||
print("CYCLING WORKOUT ANALYZER")
|
||||
print(f"{'='*60}")
|
||||
print("1. Analyze last cycling workout")
|
||||
print("2. Get next workout suggestion")
|
||||
print("3. Enhanced analysis")
|
||||
print("4. List available MCP tools")
|
||||
print("5. List available templates")
|
||||
print("6. Show cached data")
|
||||
print("7. Clear cache")
|
||||
print("8. Exit")
|
||||
print("-" * 60)
|
||||
|
||||
choice = input("Enter your choice (1-8): ").strip()
|
||||
|
||||
try:
|
||||
if choice == "1":
|
||||
await self._analyze_last_workout()
|
||||
elif choice == "2":
|
||||
await self._suggest_next_workout()
|
||||
elif choice == "3":
|
||||
await self._enhanced_analysis()
|
||||
elif choice == "4":
|
||||
await self._list_tools()
|
||||
elif choice == "5":
|
||||
self._list_templates()
|
||||
elif choice == "6":
|
||||
self._show_cached_data()
|
||||
elif choice == "7":
|
||||
self._clear_cache()
|
||||
elif choice == "8":
|
||||
break
|
||||
else:
|
||||
print("Invalid choice. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
logging.error(f"Menu action error: {e}")
|
||||
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
async def _analyze_last_workout(self):
|
||||
"""Analyze last workout"""
|
||||
print("\nAnalyzing your last workout...")
|
||||
|
||||
# Load training rules
|
||||
rules = self._load_training_rules()
|
||||
|
||||
result = await self.app.analyze_workout(
|
||||
analysis_type="analyze_last_workout",
|
||||
training_rules=rules
|
||||
)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("WORKOUT ANALYSIS")
|
||||
print("="*50)
|
||||
console = Console()
|
||||
console.print(Markdown(result))
|
||||
|
||||
async def _suggest_next_workout(self):
|
||||
"""Suggest next workout"""
|
||||
print("\nGenerating workout suggestion...")
|
||||
|
||||
# Load training rules
|
||||
rules = self._load_training_rules()
|
||||
|
||||
result = await self.app.suggest_next_workout(training_rules=rules)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("NEXT WORKOUT SUGGESTION")
|
||||
print("="*50)
|
||||
console = Console()
|
||||
console.print(Markdown(result))
|
||||
|
||||
async def _enhanced_analysis(self):
|
||||
"""Enhanced analysis menu"""
|
||||
print("\nSelect analysis type:")
|
||||
print("a) Performance trends")
|
||||
print("b) Training load analysis")
|
||||
print("c) Recovery assessment")
|
||||
|
||||
choice = input("Enter choice (a-c): ").strip().lower()
|
||||
|
||||
analysis_types = {
|
||||
'a': 'performance trends',
|
||||
'b': 'training load',
|
||||
'c': 'recovery assessment'
|
||||
}
|
||||
|
||||
if choice not in analysis_types:
|
||||
print("Invalid choice.")
|
||||
return
|
||||
|
||||
analysis_type = analysis_types[choice]
|
||||
print(f"\nPerforming {analysis_type} analysis...")
|
||||
|
||||
# Load training rules
|
||||
rules = self._load_training_rules()
|
||||
|
||||
result = await self.app.enhanced_analysis(
|
||||
analysis_type,
|
||||
training_rules=rules
|
||||
)
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"ENHANCED {analysis_type.upper()} ANALYSIS")
|
||||
print("="*50)
|
||||
console = Console()
|
||||
console.print(Markdown(result))
|
||||
|
||||
async def _list_tools(self):
|
||||
"""List available tools"""
|
||||
tools = await self.app.list_available_tools()
|
||||
if tools:
|
||||
self.app.mcp_client.print_tools()
|
||||
else:
|
||||
print("No MCP tools available")
|
||||
|
||||
def _list_templates(self):
|
||||
"""List available templates"""
|
||||
templates = self.app.list_templates()
|
||||
print(f"\nAvailable templates ({len(templates)}):")
|
||||
for template in templates:
|
||||
print(f" - {template}")
|
||||
|
||||
def _show_cached_data(self):
|
||||
"""Show cached data"""
|
||||
cached_data = self.app.get_cached_data()
|
||||
print(f"\nCached data ({len(cached_data)} items):")
|
||||
for key, value in cached_data.items():
|
||||
data_type = type(value).__name__
|
||||
if isinstance(value, (dict, list)):
|
||||
size = len(value)
|
||||
print(f" - {key}: {data_type} (size: {size})")
|
||||
else:
|
||||
print(f" - {key}: {data_type}")
|
||||
|
||||
def _clear_cache(self):
|
||||
"""Clear cache"""
|
||||
self.app.cache_manager.clear()
|
||||
print("Cache cleared")
|
||||
|
||||
def _load_training_rules(self) -> str:
|
||||
"""Load training rules from file"""
|
||||
rules_file = Path(self.app.config.rules_file)
|
||||
if rules_file.exists():
|
||||
with open(rules_file, 'r') as f:
|
||||
return f.read()
|
||||
else:
|
||||
# Create default rules
|
||||
default_rules = """
|
||||
Training Goals:
|
||||
- Improve FTP (Functional Threshold Power)
|
||||
- Build endurance for long rides
|
||||
- Maintain consistent training
|
||||
|
||||
Power Zones (adjust based on your FTP):
|
||||
- Zone 1 (Active Recovery): < 55% FTP
|
||||
- Zone 2 (Endurance): 56-75% FTP
|
||||
- Zone 3 (Tempo): 76-90% FTP
|
||||
- Zone 4 (Lactate Threshold): 91-105% FTP
|
||||
- Zone 5 (VO2 Max): 106-120% FTP
|
||||
|
||||
Weekly Structure:
|
||||
- 70-80% easy/moderate intensity
|
||||
- 20-30% high intensity
|
||||
- At least 1 rest day per week
|
||||
"""
|
||||
rules_file.parent.mkdir(exist_ok=True)
|
||||
with open(rules_file, 'w') as f:
|
||||
f.write(default_rules)
|
||||
return default_rules
|
||||
|
||||
async def _handle_command(self, args):
|
||||
"""Handle non-interactive commands"""
|
||||
if args.command == "analyze_last":
|
||||
await self._analyze_last_workout()
|
||||
else:
|
||||
print(f"Unknown command: {args.command}")
|
||||
|
||||
async def main():
|
||||
"""CLI entry point"""
|
||||
parser = argparse.ArgumentParser(description="Cycling Workout Analyzer")
|
||||
parser.add_argument('command', nargs='?', help="Command to execute (e.g., analyze_last)")
|
||||
parser.add_argument('--test', action='store_true', help="Test mode: print rendered prompt without calling LLM")
|
||||
args = parser.parse_args()
|
||||
|
||||
cli = CLI()
|
||||
await cli.run(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
285
cli/setup_garth.py
Normal file
285
cli/setup_garth.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Garth Setup Script
|
||||
Helper script to set up Garth authentication and test connection
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import garth
|
||||
print("✅ Garth module available")
|
||||
except ImportError:
|
||||
print("❌ Garth module not found")
|
||||
print("Install with: pip install garth")
|
||||
sys.exit(1)
|
||||
|
||||
def setup_garth_auth():
|
||||
"""Setup Garth authentication"""
|
||||
print("🔐 Setting up Garth authentication...")
|
||||
print("This will guide you through authenticating with Garmin Connect")
|
||||
print()
|
||||
|
||||
# Check if session already exists
|
||||
session_path = Path.home() / ".garth"
|
||||
if session_path.exists():
|
||||
print("📁 Existing Garth session found")
|
||||
choice = input("Do you want to use existing session? (y/n): ").strip().lower()
|
||||
if choice == 'y':
|
||||
try:
|
||||
garth.resume(str(session_path))
|
||||
# Test session
|
||||
test_result = test_garth_connection()
|
||||
if test_result:
|
||||
print("✅ Existing session is valid")
|
||||
return True
|
||||
else:
|
||||
print("❌ Existing session is invalid, creating new one...")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not resume session: {e}")
|
||||
print("Creating new session...")
|
||||
|
||||
# Create new session
|
||||
print("\n🆕 Creating new Garth session...")
|
||||
print("You'll need your Garmin Connect credentials")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Configure Garth
|
||||
garth.configure()
|
||||
|
||||
# Get credentials
|
||||
username = input("Garmin Connect username/email: ").strip()
|
||||
password = input("Garmin Connect password: ").strip()
|
||||
|
||||
if not username or not password:
|
||||
print("❌ Username and password are required")
|
||||
return False
|
||||
|
||||
print("\n🔄 Logging into Garmin Connect...")
|
||||
|
||||
# Login
|
||||
garth.login(username, password)
|
||||
|
||||
# Save session
|
||||
garth.save(str(session_path))
|
||||
print(f"💾 Session saved to {session_path}")
|
||||
|
||||
# Test connection
|
||||
if test_garth_connection():
|
||||
print("✅ Authentication successful!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Authentication failed")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication error: {e}")
|
||||
return False
|
||||
|
||||
def test_garth_connection():
|
||||
"""Test Garth connection"""
|
||||
print("🧪 Testing Garth connection...")
|
||||
|
||||
# Use the endpoint we know works
|
||||
try:
|
||||
print(" Trying social profile endpoint...")
|
||||
user_info = garth.connectapi("/userprofile-service/socialProfile")
|
||||
|
||||
if user_info and isinstance(user_info, dict):
|
||||
display_name = user_info.get('displayName', 'Unknown')
|
||||
full_name = user_info.get('fullName', 'Unknown')
|
||||
print(f"✅ Connected as: {display_name} ({full_name})")
|
||||
|
||||
# Test activities too
|
||||
try:
|
||||
activities = garth.connectapi("/activitylist-service/activities/search/activities", params={"limit": 1})
|
||||
if activities and len(activities) > 0:
|
||||
print(f"✅ Activities accessible: Found {len(activities)} recent activity")
|
||||
else:
|
||||
print(f"⚠️ Activities accessible but none found")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Activities test failed: {str(e)[:50]}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print("❌ Invalid response from social profile endpoint")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Connection test failed: {e}")
|
||||
return False
|
||||
|
||||
def extract_token_info():
|
||||
"""Extract token information for config"""
|
||||
print("\n🎫 Extracting token information...")
|
||||
|
||||
session_path = Path.home() / ".garth"
|
||||
if not session_path.exists():
|
||||
print("❌ No Garth session found. Run authentication first.")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Load session data
|
||||
with open(session_path, 'r') as f:
|
||||
session_data = json.load(f)
|
||||
|
||||
# Extract token (this might need adjustment based on Garth's session format)
|
||||
token = session_data.get('oauth_token') or session_data.get('token')
|
||||
|
||||
if token:
|
||||
print(f"✅ Token extracted: {token[:20]}...")
|
||||
print("\n📝 Add this to your config.yaml:")
|
||||
print(f"garth_token: \"{token}\"")
|
||||
return token
|
||||
else:
|
||||
print("❌ Could not extract token from session")
|
||||
print("Available session keys:", list(session_data.keys()))
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error extracting token: {e}")
|
||||
return None
|
||||
|
||||
def show_user_info():
|
||||
"""Show current user information"""
|
||||
print("\n👤 User Information:")
|
||||
|
||||
# Try multiple endpoints to get user info
|
||||
endpoints_to_try = [
|
||||
("/userprofile-service/socialProfile", "Social Profile"),
|
||||
("/user-service/users/settings", "User Settings"),
|
||||
("/modern/currentuser-service/user/profile", "User Profile"),
|
||||
("/userprofile-service/userprofile", "User Profile Alt"),
|
||||
]
|
||||
|
||||
user_info = None
|
||||
working_endpoint = None
|
||||
|
||||
for endpoint, name in endpoints_to_try:
|
||||
try:
|
||||
print(f" Trying {name}...")
|
||||
data = garth.connectapi(endpoint)
|
||||
if data and isinstance(data, dict):
|
||||
user_info = data
|
||||
working_endpoint = endpoint
|
||||
print(f" ✅ {name} successful")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" ❌ {name} failed: {str(e)[:100]}")
|
||||
continue
|
||||
|
||||
if user_info:
|
||||
print(f"\n Working Endpoint: {working_endpoint}")
|
||||
print(f" Data keys available: {list(user_info.keys())}")
|
||||
|
||||
# Extract common fields
|
||||
display_name = (
|
||||
user_info.get('displayName') or
|
||||
user_info.get('fullName') or
|
||||
user_info.get('userName') or
|
||||
user_info.get('profileDisplayName') or
|
||||
'Unknown'
|
||||
)
|
||||
|
||||
user_id = (
|
||||
user_info.get('id') or
|
||||
user_info.get('userId') or
|
||||
user_info.get('profileId') or
|
||||
'Unknown'
|
||||
)
|
||||
|
||||
print(f" Display Name: {display_name}")
|
||||
print(f" User ID: {user_id}")
|
||||
|
||||
# Show first few fields for debugging
|
||||
print(f" Sample data:")
|
||||
for key, value in list(user_info.items())[:5]:
|
||||
print(f" {key}: {str(value)[:50]}")
|
||||
|
||||
# Try to get activities
|
||||
try:
|
||||
print(f"\n Testing activities access...")
|
||||
activities = garth.connectapi("/activitylist-service/activities/search/activities",
|
||||
params={"limit": 1})
|
||||
if activities and len(activities) > 0:
|
||||
print(f" Recent Activities: ✅ Available ({len(activities)} found)")
|
||||
activity = activities[0]
|
||||
activity_type = activity.get('activityType', {})
|
||||
if isinstance(activity_type, dict):
|
||||
type_name = activity_type.get('typeKey', 'Unknown')
|
||||
else:
|
||||
type_name = str(activity_type)
|
||||
print(f" Last Activity: {type_name}")
|
||||
else:
|
||||
print(f" Recent Activities: ⚠️ None found")
|
||||
except Exception as e:
|
||||
print(f" Recent Activities: ❌ Error ({str(e)[:50]})")
|
||||
else:
|
||||
print(" ❌ Could not retrieve any user information")
|
||||
|
||||
# Show what endpoints are available
|
||||
print("\n Available endpoints check:")
|
||||
test_endpoints = [
|
||||
"/userstats-service/statistics",
|
||||
"/wellness-service/wellness",
|
||||
"/device-service/deviceRegistration",
|
||||
]
|
||||
|
||||
for endpoint in test_endpoints:
|
||||
try:
|
||||
garth.connectapi(endpoint)
|
||||
print(f" ✅ {endpoint} - Available")
|
||||
except Exception as e:
|
||||
if "404" in str(e):
|
||||
print(f" ❌ {endpoint} - Not Found")
|
||||
elif "403" in str(e):
|
||||
print(f" ⚠️ {endpoint} - Forbidden (auth issue)")
|
||||
else:
|
||||
print(f" ❌ {endpoint} - Error: {str(e)[:30]}")
|
||||
|
||||
def main():
|
||||
"""Main setup function"""
|
||||
print("Garth Setup and Authentication")
|
||||
print("=" * 40)
|
||||
|
||||
while True:
|
||||
print("\nOptions:")
|
||||
print("1. Setup/Login to Garmin Connect")
|
||||
print("2. Test existing connection")
|
||||
print("3. Show user information")
|
||||
print("4. Extract token for config")
|
||||
print("5. Exit")
|
||||
|
||||
choice = input("\nEnter choice (1-5): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
success = setup_garth_auth()
|
||||
if success:
|
||||
show_user_info()
|
||||
|
||||
elif choice == "2":
|
||||
test_garth_connection()
|
||||
|
||||
elif choice == "3":
|
||||
show_user_info()
|
||||
|
||||
elif choice == "4":
|
||||
token = extract_token_info()
|
||||
if token:
|
||||
# Also show how to set environment variable
|
||||
print(f"\nOr set as environment variable:")
|
||||
print(f"export GARTH_TOKEN=\"{token}\"")
|
||||
|
||||
elif choice == "5":
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
|
||||
else:
|
||||
print("Invalid choice. Please try again.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user