206 lines
6.9 KiB
Python
206 lines
6.9 KiB
Python
|
|
"""
|
|||
|
|
Unified launcher for ajarbot with pre-flight checks.
|
|||
|
|
|
|||
|
|
This launcher:
|
|||
|
|
1. Performs environment checks (Node.js, Claude CLI auth)
|
|||
|
|
2. Sets sensible defaults (agent-sdk mode)
|
|||
|
|
3. Delegates to bot_runner.main() for actual execution
|
|||
|
|
|
|||
|
|
Usage:
|
|||
|
|
python ajarbot.py # Run with default config
|
|||
|
|
python ajarbot.py --config custom.yaml # Use custom config file
|
|||
|
|
python ajarbot.py --init # Generate config template
|
|||
|
|
python ajarbot.py --setup-google # Set up Google OAuth
|
|||
|
|
python ajarbot.py --health # Run health check
|
|||
|
|
|
|||
|
|
Environment variables:
|
|||
|
|
AJARBOT_LLM_MODE # LLM mode: "agent-sdk" or "api" (default: agent-sdk)
|
|||
|
|
AJARBOT_SLACK_BOT_TOKEN # Slack bot token (xoxb-...)
|
|||
|
|
AJARBOT_SLACK_APP_TOKEN # Slack app token (xapp-...)
|
|||
|
|
AJARBOT_TELEGRAM_BOT_TOKEN # Telegram bot token
|
|||
|
|
ANTHROPIC_API_KEY # Claude API key (only needed for api mode)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import shutil
|
|||
|
|
import subprocess
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
|
|||
|
|
class PreflightCheck:
|
|||
|
|
"""Performs environment checks before launching the bot."""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.warnings = []
|
|||
|
|
self.errors = []
|
|||
|
|
|
|||
|
|
def check_nodejs(self) -> bool:
|
|||
|
|
"""Check if Node.js is available (required for agent-sdk mode)."""
|
|||
|
|
try:
|
|||
|
|
result = subprocess.run(
|
|||
|
|
["node", "--version"],
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
timeout=5
|
|||
|
|
)
|
|||
|
|
if result.returncode == 0:
|
|||
|
|
version = result.stdout.strip()
|
|||
|
|
print(f"✓ Node.js found: {version}")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self.warnings.append("Node.js not found (required for agent-sdk mode)")
|
|||
|
|
return False
|
|||
|
|
except FileNotFoundError:
|
|||
|
|
self.warnings.append("Node.js not found (required for agent-sdk mode)")
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
self.warnings.append(f"Error checking Node.js: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def check_claude_cli_auth(self) -> bool:
|
|||
|
|
"""Check if Claude CLI is authenticated."""
|
|||
|
|
try:
|
|||
|
|
result = subprocess.run(
|
|||
|
|
["claude", "auth", "status"],
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
timeout=5
|
|||
|
|
)
|
|||
|
|
if result.returncode == 0 and "Authenticated" in result.stdout:
|
|||
|
|
print("✓ Claude CLI authenticated")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self.warnings.append("Claude CLI not authenticated (run: claude auth login)")
|
|||
|
|
return False
|
|||
|
|
except FileNotFoundError:
|
|||
|
|
self.warnings.append("Claude CLI not found (install from: https://claude.ai/download)")
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
self.warnings.append(f"Error checking Claude CLI: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def check_python_version(self) -> bool:
|
|||
|
|
"""Check if Python version is compatible."""
|
|||
|
|
version_info = sys.version_info
|
|||
|
|
if version_info >= (3, 10):
|
|||
|
|
print(f"✓ Python {version_info.major}.{version_info.minor}.{version_info.micro}")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self.errors.append(
|
|||
|
|
f"Python 3.10+ required (found {version_info.major}.{version_info.minor}.{version_info.micro})"
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def check_env_file(self) -> bool:
|
|||
|
|
"""Check if .env file exists (for API key storage)."""
|
|||
|
|
env_path = Path(".env")
|
|||
|
|
if env_path.exists():
|
|||
|
|
print(f"✓ .env file found")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self.warnings.append(".env file not found (create one if using API mode)")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def check_config_file(self) -> bool:
|
|||
|
|
"""Check if adapter config exists."""
|
|||
|
|
config_path = Path("config/adapters.local.yaml")
|
|||
|
|
if config_path.exists():
|
|||
|
|
print(f"✓ Config file found: {config_path}")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self.warnings.append(
|
|||
|
|
"config/adapters.local.yaml not found (run: python ajarbot.py --init)"
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def set_default_llm_mode(self):
|
|||
|
|
"""Set default LLM mode to agent-sdk if not specified."""
|
|||
|
|
if "AJARBOT_LLM_MODE" not in os.environ:
|
|||
|
|
os.environ["AJARBOT_LLM_MODE"] = "agent-sdk"
|
|||
|
|
print("ℹ Using LLM mode: agent-sdk (default)")
|
|||
|
|
else:
|
|||
|
|
mode = os.environ["AJARBOT_LLM_MODE"]
|
|||
|
|
print(f"ℹ Using LLM mode: {mode} (from environment)")
|
|||
|
|
|
|||
|
|
def run_all_checks(self) -> bool:
|
|||
|
|
"""Run all pre-flight checks. Returns True if safe to proceed."""
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("Ajarbot Pre-Flight Checks")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# Critical checks
|
|||
|
|
self.check_python_version()
|
|||
|
|
|
|||
|
|
# LLM mode dependent checks
|
|||
|
|
llm_mode = os.environ.get("AJARBOT_LLM_MODE", "agent-sdk")
|
|||
|
|
|
|||
|
|
if llm_mode == "agent-sdk":
|
|||
|
|
print("\n[Agent SDK Mode Checks]")
|
|||
|
|
self.check_nodejs()
|
|||
|
|
self.check_claude_cli_auth()
|
|||
|
|
elif llm_mode == "api":
|
|||
|
|
print("\n[API Mode Checks]")
|
|||
|
|
has_env = self.check_env_file()
|
|||
|
|
if has_env:
|
|||
|
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|||
|
|
self.errors.append("ANTHROPIC_API_KEY not set in .env file (required for API mode)")
|
|||
|
|
else:
|
|||
|
|
self.errors.append(".env file with ANTHROPIC_API_KEY required for API mode")
|
|||
|
|
|
|||
|
|
# Common checks
|
|||
|
|
print("\n[Configuration Checks]")
|
|||
|
|
self.check_config_file()
|
|||
|
|
|
|||
|
|
# Display results
|
|||
|
|
print()
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
if self.errors:
|
|||
|
|
print("ERRORS (must fix before running):")
|
|||
|
|
for error in self.errors:
|
|||
|
|
print(f" ✗ {error}")
|
|||
|
|
print()
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if self.warnings:
|
|||
|
|
print("WARNINGS (optional, but recommended):")
|
|||
|
|
for warning in self.warnings:
|
|||
|
|
print(f" ⚠ {warning}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
print("Pre-flight checks complete!")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print()
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""Main entry point with pre-flight checks."""
|
|||
|
|
# Set default LLM mode before checks
|
|||
|
|
checker = PreflightCheck()
|
|||
|
|
checker.set_default_llm_mode()
|
|||
|
|
|
|||
|
|
# Special commands that bypass pre-flight checks
|
|||
|
|
bypass_commands = ["--init", "--help", "-h"]
|
|||
|
|
if any(arg in sys.argv for arg in bypass_commands):
|
|||
|
|
# Import and run bot_runner directly
|
|||
|
|
from bot_runner import main as bot_main
|
|||
|
|
bot_main()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Run pre-flight checks for normal operation
|
|||
|
|
if not checker.run_all_checks():
|
|||
|
|
print("\nPre-flight checks failed. Please fix the errors above.")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# All checks passed - delegate to bot_runner
|
|||
|
|
print("Launching ajarbot...\n")
|
|||
|
|
from bot_runner import main as bot_main
|
|||
|
|
bot_main()
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|