Implemented Phase 1 of ClaudeTools Discord bot with: Core Features: - Discord.py bot with message content intents - Claude API integration with streaming responses - Thread-based conversations with context management - @mention handling with automatic thread creation - Tool definitions for future ClaudeTools/remediation integration Architecture: - bot/main.py: Entry point with Discord client setup - bot/config.py: Pydantic Settings for environment config - bot/claude/client.py: Anthropic SDK wrapper with streaming - bot/claude/tools.py: Tool definitions and system prompt - bot/handlers/message_handler.py: Discord message handling Configuration: - requirements.txt: Python dependencies (discord.py, anthropic, httpx) - .env.example: Environment variable template - .gitignore: Sensitive data protection - README.md: Comprehensive setup and usage guide Next Steps (Phase 2): - Implement tool execution (ClaudeTools API client) - Add user role mapping and permissions - Implement audit logging Deployment Target: BEAST (Windows) as NSSM service Test: @ClaudeTools hello should create thread and stream response Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
137 lines
3.8 KiB
Python
137 lines
3.8 KiB
Python
"""ClaudeTools Discord Bot - Main Entry Point."""
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
from dotenv import load_dotenv
|
|
|
|
from bot.config import settings
|
|
from bot.claude.client import ClaudeClient
|
|
from bot.handlers.message_handler import MessageHandler
|
|
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.log_level.upper()),
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout),
|
|
]
|
|
)
|
|
|
|
# Add file handler if log file specified
|
|
if settings.log_file:
|
|
settings.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
file_handler = logging.FileHandler(settings.log_file)
|
|
file_handler.setFormatter(
|
|
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
)
|
|
logging.getLogger().addHandler(file_handler)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Discord bot setup with required intents
|
|
intents = discord.Intents.default()
|
|
intents.message_content = True # Required to read message content
|
|
intents.guilds = True
|
|
intents.members = True
|
|
|
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
|
|
|
|
|
# Initialize Claude client
|
|
claude_client = ClaudeClient()
|
|
|
|
# Initialize message handler
|
|
message_handler: MessageHandler = None
|
|
|
|
|
|
@bot.event
|
|
async def on_ready():
|
|
"""Called when the bot successfully connects to Discord."""
|
|
global message_handler
|
|
|
|
logger.info(f"Bot connected as {bot.user.name} (ID: {bot.user.id})")
|
|
logger.info(f"Connected to {len(bot.guilds)} guild(s)")
|
|
|
|
# Validate paths
|
|
try:
|
|
settings.validate_paths()
|
|
logger.info("Path validation successful")
|
|
logger.info(f"Vault path: {settings.vault_path}")
|
|
logger.info(f"ClaudeTools root: {settings.claudetools_root}")
|
|
logger.info(f"Git Bash: {settings.git_bash_path}")
|
|
except FileNotFoundError as e:
|
|
logger.error(f"Path validation failed: {e}")
|
|
logger.error("Bot will continue but some features may not work")
|
|
|
|
# Initialize message handler
|
|
message_handler = MessageHandler(bot, claude_client)
|
|
|
|
# Set bot status
|
|
await bot.change_presence(
|
|
activity=discord.Activity(
|
|
type=discord.ActivityType.watching,
|
|
name="for @mentions | ClaudeTools MSP Assistant"
|
|
)
|
|
)
|
|
|
|
logger.info("Bot is ready and listening for mentions!")
|
|
|
|
|
|
@bot.event
|
|
async def on_message(message: discord.Message):
|
|
"""Called when a message is sent in a channel the bot can see."""
|
|
# Ignore messages from bots
|
|
if message.author.bot:
|
|
return
|
|
|
|
# Check if bot was mentioned
|
|
if bot.user in message.mentions:
|
|
logger.info(
|
|
f"Mentioned by {message.author.name} in #{message.channel.name}: "
|
|
f"{message.content[:100]}"
|
|
)
|
|
await message_handler.handle_mention(message)
|
|
return
|
|
|
|
# Process commands (for future slash commands)
|
|
await bot.process_commands(message)
|
|
|
|
|
|
@bot.event
|
|
async def on_error(event: str, *args, **kwargs):
|
|
"""Called when an error occurs."""
|
|
logger.error(f"Error in {event}", exc_info=sys.exc_info())
|
|
|
|
|
|
async def main():
|
|
"""Main entry point."""
|
|
try:
|
|
logger.info("Starting ClaudeTools Discord Bot...")
|
|
logger.info(f"Claude Model: {settings.claude_model}")
|
|
logger.info(f"ClaudeTools API: {settings.claudetools_api_url}")
|
|
|
|
# Start the bot
|
|
async with bot:
|
|
await bot.start(settings.discord_token)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Received keyboard interrupt, shutting down...")
|
|
except Exception as e:
|
|
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
raise
|
|
finally:
|
|
logger.info("Bot shut down complete")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|