diff --git a/.env b/.env new file mode 100644 index 0000000..5558e48 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# Slack Bot Configuration +SLACK_BOT_TOKEN=xoxb-your-bot-token-here +SLACK_CHANNEL=#general +SLACK_APP_TOKEN=xapp-your-app-token-here + +# Agent Configuration +UPDATE_INTERVAL=3600 # Update interval in seconds (1 hour) +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5558e48 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Slack Bot Configuration +SLACK_BOT_TOKEN=xoxb-your-bot-token-here +SLACK_CHANNEL=#general +SLACK_APP_TOKEN=xapp-your-app-token-here + +# Agent Configuration +UPDATE_INTERVAL=3600 # Update interval in seconds (1 hour) +LOG_LEVEL=INFO \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..30d7e13 --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ +# Slack Welcome Message Agent + +An intelligent agent that automatically updates and manages Slack welcome messages for the Cursor bot integration. + +## Features + +- šŸ¤– **Automated Updates**: Periodically updates welcome messages +- šŸ“ **Configurable Messages**: JSON-based message configuration +- šŸ”„ **Real-time Monitoring**: Continuous monitoring and status reporting +- šŸ› ļø **Easy Management**: Simple start/stop/restart controls +- šŸ“Š **Rich Formatting**: Support for Slack blocks and rich text +- šŸ”§ **Flexible Configuration**: Environment-based configuration + +## Quick Start + +1. **Setup the environment:** + ```bash + chmod +x setup.sh + ./setup.sh + ``` + +2. **Configure your Slack token:** + ```bash + cp .env.example .env + # Edit .env and add your SLACK_BOT_TOKEN + ``` + +3. **Start the agent:** + ```bash + python agent_controller.py start + ``` + +4. **Check status:** + ```bash + python agent_controller.py status + ``` + +## Configuration + +### Environment Variables + +Create a `.env` file with the following variables: + +```env +# Required +SLACK_BOT_TOKEN=xoxb-your-bot-token-here + +# Optional +SLACK_CHANNEL=#general +UPDATE_INTERVAL=3600 +LOG_LEVEL=INFO +``` + +### Welcome Messages + +Edit `welcome_config.json` to customize your welcome messages: + +```json +{ + "messages": [ + { + "text": "Welcome to Cursor for Slack!", + "blocks": [...], + "channel": "#general" + } + ] +} +``` + +## Agent Commands + +### Start the Agent +```bash +python agent_controller.py start +``` + +### Stop the Agent +```bash +python agent_controller.py stop +``` + +### Restart the Agent +```bash +python agent_controller.py restart +``` + +### Check Status +```bash +python agent_controller.py status +``` + +### Manual Message Update +```bash +python agent_controller.py update "New welcome message" "#channel" +``` + +## Slack Bot Setup + +1. **Create a Slack App** at https://api.slack.com/apps +2. **Add Bot Token Scopes:** + - `chat:write` + - `chat:write.public` + - `channels:read` + - `groups:read` +3. **Install the app** to your workspace +4. **Copy the Bot User OAuth Token** (starts with `xoxb-`) + +## Architecture + +### Core Components + +- **`slack_welcome_agent.py`**: Main agent implementation +- **`agent_controller.py`**: Management interface +- **`welcome_config.json`**: Message configuration +- **`requirements.txt`**: Python dependencies +- **`.env`**: Environment configuration + +### How It Works + +1. The agent loads message configurations from `welcome_config.json` +2. It connects to Slack using the WebClient API +3. Messages are posted according to the configured schedule +4. The controller manages the agent lifecycle (start/stop/status) +5. Logs are written to `slack_agent.log` for monitoring + +## Message Format + +Welcome messages support both simple text and rich Slack Block Kit formatting: + +```json +{ + "text": "Fallback text", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Welcome to Cursor!* :cursor:" + } + } + ] +} +``` + +## Monitoring & Logs + +- **Log File**: `slack_agent.log` +- **Status Check**: `python agent_controller.py status` +- **Process Monitoring**: Uses PID file for process tracking + +## Troubleshooting + +### Agent Won't Start +1. Check if `SLACK_BOT_TOKEN` is set in `.env` +2. Verify Slack bot permissions +3. Check logs in `slack_agent.log` + +### Messages Not Posting +1. Verify bot has `chat:write` permission +2. Check if bot is added to the target channel +3. Review Slack API error messages in logs + +### Connection Issues +1. Test with: `python -c "from slack_welcome_agent import SlackWelcomeAgent; import asyncio; import os; agent = SlackWelcomeAgent(os.getenv('SLACK_BOT_TOKEN')); print(asyncio.run(agent.test_connection()))"` +2. Verify token format (should start with `xoxb-`) +3. Check network connectivity + +## Development + +### Running in Development Mode +```bash +# Install development dependencies +pip install -r requirements.txt + +# Run agent directly +python slack_welcome_agent.py + +# Or use the controller +python agent_controller.py start +``` + +### Testing +```bash +# Test Slack connection +python agent_controller.py status + +# Send test message +python agent_controller.py update "Test message" "#test-channel" +``` + +## License + +This project is open source and available under the MIT License. \ No newline at end of file diff --git a/__pycache__/agent_controller.cpython-313.pyc b/__pycache__/agent_controller.cpython-313.pyc new file mode 100644 index 0000000..1fa5b3c Binary files /dev/null and b/__pycache__/agent_controller.cpython-313.pyc differ diff --git a/agent_controller.py b/agent_controller.py new file mode 100755 index 0000000..9223853 --- /dev/null +++ b/agent_controller.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +Slack Welcome Agent Controller + +This script provides control interface for the Slack welcome message agent. +Use this to start, stop, check status, and manage the agent. +""" + +import os +import sys +import json +import signal +import subprocess +import time +from pathlib import Path +import psutil + +class AgentController: + """Controller for managing the Slack welcome agent""" + + def __init__(self): + self.agent_script = "/workspace/slack_welcome_agent.py" + self.pid_file = "/workspace/agent.pid" + self.log_file = "/workspace/slack_agent.log" + + def is_running(self) -> bool: + """Check if the agent is currently running""" + if not os.path.exists(self.pid_file): + return False + + try: + with open(self.pid_file, 'r') as f: + pid = int(f.read().strip()) + + # Check if process exists and is our agent + if psutil.pid_exists(pid): + proc = psutil.Process(pid) + if 'slack_welcome_agent.py' in ' '.join(proc.cmdline()): + return True + except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied): + pass + + # Clean up stale PID file + self._cleanup_pid_file() + return False + + def start(self) -> bool: + """Start the agent""" + if self.is_running(): + print("Agent is already running") + return True + + # Check if environment is configured + if not os.getenv('SLACK_BOT_TOKEN'): + print("ERROR: SLACK_BOT_TOKEN environment variable not set") + print("Please set your Slack bot token or copy .env.example to .env and configure it") + return False + + try: + # Start the agent in background + process = subprocess.Popen( + [sys.executable, self.agent_script], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True + ) + + # Save PID + with open(self.pid_file, 'w') as f: + f.write(str(process.pid)) + + # Give it a moment to start + time.sleep(2) + + if self.is_running(): + print(f"Agent started successfully (PID: {process.pid})") + print(f"Logs: {self.log_file}") + return True + else: + print("Failed to start agent") + return False + + except Exception as e: + print(f"Error starting agent: {e}") + return False + + def stop(self) -> bool: + """Stop the agent""" + if not self.is_running(): + print("Agent is not running") + return True + + try: + with open(self.pid_file, 'r') as f: + pid = int(f.read().strip()) + + # Send SIGTERM + os.kill(pid, signal.SIGTERM) + + # Wait for graceful shutdown + for _ in range(10): + if not self.is_running(): + print("Agent stopped successfully") + return True + time.sleep(1) + + # Force kill if needed + try: + os.kill(pid, signal.SIGKILL) + print("Agent force-stopped") + except ProcessLookupError: + pass + + self._cleanup_pid_file() + return True + + except Exception as e: + print(f"Error stopping agent: {e}") + return False + + def restart(self) -> bool: + """Restart the agent""" + print("Restarting agent...") + self.stop() + time.sleep(2) + return self.start() + + def status(self): + """Show agent status""" + running = self.is_running() + print(f"Agent Status: {'RUNNING' if running else 'STOPPED'}") + + if running: + try: + with open(self.pid_file, 'r') as f: + pid = int(f.read().strip()) + proc = psutil.Process(pid) + print(f"PID: {pid}") + print(f"Memory Usage: {proc.memory_info().rss / 1024 / 1024:.1f} MB") + print(f"CPU Percent: {proc.cpu_percent():.1f}%") + print(f"Started: {proc.create_time()}") + except Exception: + print("Could not get detailed status") + + # Show recent logs + if os.path.exists(self.log_file): + print(f"\nRecent logs ({self.log_file}):") + try: + with open(self.log_file, 'r') as f: + lines = f.readlines() + for line in lines[-10:]: # Last 10 lines + print(f" {line.strip()}") + except Exception: + print(" Could not read log file") + + def update_message(self, text: str, channel: str = None): + """Manually trigger a welcome message update""" + if not self.is_running(): + print("Agent is not running. Starting temporarily...") + # Run a one-time update + try: + import asyncio + from slack_welcome_agent import SlackWelcomeAgent, WelcomeMessage + + token = os.getenv('SLACK_BOT_TOKEN') + if not token: + print("ERROR: SLACK_BOT_TOKEN not set") + return + + agent = SlackWelcomeAgent(token) + message = WelcomeMessage(text=text, channel=channel) + success = asyncio.run(agent.update_welcome_message(message, channel)) + + if success: + print("Welcome message updated successfully") + else: + print("Failed to update welcome message") + + except Exception as e: + print(f"Error updating message: {e}") + else: + print("Agent is running - it will update messages automatically") + print("To modify the welcome message, edit welcome_config.json") + + def _cleanup_pid_file(self): + """Remove stale PID file""" + try: + os.remove(self.pid_file) + except FileNotFoundError: + pass + +def main(): + """Main CLI interface""" + controller = AgentController() + + if len(sys.argv) < 2: + print("Usage: python agent_controller.py ") + print("Commands: start, stop, restart, status, update") + return + + command = sys.argv[1].lower() + + if command == "start": + controller.start() + elif command == "stop": + controller.stop() + elif command == "restart": + controller.restart() + elif command == "status": + controller.status() + elif command == "update": + text = sys.argv[2] if len(sys.argv) > 2 else "Welcome to Cursor for Slack! Here's how to use it:" + channel = sys.argv[3] if len(sys.argv) > 3 else None + controller.update_message(text, channel) + else: + print(f"Unknown command: {command}") + print("Available commands: start, stop, restart, status, update") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/quick_status.py b/quick_status.py new file mode 100644 index 0000000..93735b1 --- /dev/null +++ b/quick_status.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Quick status check for the Slack Welcome Message Agent +""" + +import os +import sys +from agent_controller import AgentController + +def main(): + """Quick status check and summary""" + controller = AgentController() + + print("šŸ¤– Slack Welcome Message Agent Status") + print("=" * 40) + + # Check if agent is running + is_running = controller.is_running() + status_emoji = "🟢" if is_running else "šŸ”“" + status_text = "RUNNING" if is_running else "STOPPED" + + print(f"{status_emoji} Status: {status_text}") + + # Check configuration + env_file = "/workspace/.env" + config_file = "/workspace/welcome_config.json" + + print(f"šŸ“ Config Files:") + print(f" .env file: {'āœ…' if os.path.exists(env_file) else 'āŒ'}") + print(f" welcome_config.json: {'āœ…' if os.path.exists(config_file) else 'āŒ'}") + + # Check environment variables + slack_token = os.getenv('SLACK_BOT_TOKEN') + print(f"šŸ”‘ Slack Token: {'āœ… Configured' if slack_token else 'āŒ Not set'}") + + if is_running: + print(f"šŸ“Š Agent Details:") + controller.status() + else: + print("\nšŸ’” To start the agent:") + print(" 1. Set SLACK_BOT_TOKEN in .env file") + print(" 2. Run: python3 agent_controller.py start") + + print("\nšŸ› ļø Available Commands:") + print(" python3 agent_controller.py start # Start agent") + print(" python3 agent_controller.py stop # Stop agent") + print(" python3 agent_controller.py status # Detailed status") + print(" python3 quick_status.py # Quick overview") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a3638c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +slack-sdk>=3.19.0 +python-dotenv>=1.0.0 +asyncio \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..c2ce5b9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Setup script for Slack Welcome Message Agent + +echo "Setting up Slack Welcome Message Agent..." + +# Install dependencies directly (virtual env not needed in this environment) +echo "Installing dependencies..." +python3 -m pip install --break-system-packages -r requirements.txt + +# Install additional system dependencies +python3 -m pip install --break-system-packages python-dotenv psutil + +# Create .env file if it doesn't exist +if [ ! -f ".env" ]; then + echo "Creating .env file from template..." + cp .env.example .env + echo "" + echo "āš ļø IMPORTANT: Please edit .env file and add your Slack bot token!" + echo " Get your token from: https://api.slack.com/apps" + echo "" +fi + +# Make scripts executable +chmod +x slack_welcome_agent.py +chmod +x agent_controller.py + +echo "Setup complete!" +echo "" +echo "Next steps:" +echo "1. Edit .env file with your Slack bot token" +echo "2. Run: python agent_controller.py start" +echo "3. Check status: python agent_controller.py status" +echo "" +echo "Usage commands:" +echo " python agent_controller.py start # Start the agent" +echo " python agent_controller.py stop # Stop the agent" +echo " python agent_controller.py restart # Restart the agent" +echo " python agent_controller.py status # Check agent status" +echo " python agent_controller.py update 'New message' '#channel' # Manual update" \ No newline at end of file diff --git a/slack_welcome_agent.py b/slack_welcome_agent.py new file mode 100755 index 0000000..87dca21 --- /dev/null +++ b/slack_welcome_agent.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +""" +Slack Welcome Message Update Agent + +This agent monitors and updates the Slack welcome message for the Cursor bot. +It can be configured to update messages based on various triggers like time, +events, or manual updates. +""" + +import os +import json +import time +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Optional +import asyncio +from dataclasses import dataclass + +try: + from slack_sdk import WebClient + from slack_sdk.errors import SlackApiError +except ImportError: + print("Installing slack_sdk...") + import subprocess + subprocess.check_call(["pip", "install", "slack-sdk"]) + from slack_sdk import WebClient + from slack_sdk.errors import SlackApiError + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/workspace/slack_agent.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +@dataclass +class WelcomeMessage: + """Data class for welcome message configuration""" + text: str + blocks: Optional[List[Dict]] = None + channel: Optional[str] = None + thread_ts: Optional[str] = None + +class SlackWelcomeAgent: + """Agent responsible for updating Slack welcome messages""" + + def __init__(self, token: str, config_file: str = "/workspace/welcome_config.json"): + """ + Initialize the Slack Welcome Agent + + Args: + token: Slack bot token + config_file: Path to welcome message configuration file + """ + self.client = WebClient(token=token) + self.config_file = config_file + self.running = False + self.welcome_messages = self._load_welcome_messages() + + def _load_welcome_messages(self) -> List[WelcomeMessage]: + """Load welcome message configurations from file""" + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r') as f: + data = json.load(f) + return [WelcomeMessage(**msg) for msg in data.get('messages', [])] + else: + # Create default welcome messages + default_messages = [ + { + "text": "Welcome to Cursor for Slack! Here's how to use it:", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Welcome to Cursor for Slack!* :cursor:\n\nHere's how to get started:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "• Type `@Cursor help` for assistance\n• Use `@Cursor code` to get coding help\n• Try `@Cursor review` for code reviews\n• Say `@Cursor explain` to understand code" + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": f"_Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}_" + } + ] + } + ] + } + ] + self._save_welcome_messages(default_messages) + return [WelcomeMessage(**msg) for msg in default_messages] + except Exception as e: + logger.error(f"Failed to load welcome messages: {e}") + return [] + + def _save_welcome_messages(self, messages: List[Dict]): + """Save welcome messages to configuration file""" + try: + config = {"messages": messages} + with open(self.config_file, 'w') as f: + json.dump(config, f, indent=2) + logger.info(f"Saved welcome messages to {self.config_file}") + except Exception as e: + logger.error(f"Failed to save welcome messages: {e}") + + async def update_welcome_message(self, message: WelcomeMessage, channel: str = None) -> bool: + """ + Update a welcome message in Slack + + Args: + message: WelcomeMessage object containing the message data + channel: Override channel for the message + + Returns: + bool: True if successful, False otherwise + """ + try: + target_channel = channel or message.channel or os.getenv('SLACK_CHANNEL', '#general') + + if message.blocks: + response = self.client.chat_postMessage( + channel=target_channel, + text=message.text, + blocks=message.blocks, + thread_ts=message.thread_ts + ) + else: + response = self.client.chat_postMessage( + channel=target_channel, + text=message.text, + thread_ts=message.thread_ts + ) + + logger.info(f"Successfully posted welcome message to {target_channel}") + return True + + except SlackApiError as e: + logger.error(f"Slack API error: {e.response['error']}") + return False + except Exception as e: + logger.error(f"Failed to update welcome message: {e}") + return False + + async def update_all_welcome_messages(self, channel: str = None) -> int: + """ + Update all configured welcome messages + + Args: + channel: Override channel for all messages + + Returns: + int: Number of successfully updated messages + """ + success_count = 0 + for message in self.welcome_messages: + if await self.update_welcome_message(message, channel): + success_count += 1 + + logger.info(f"Updated {success_count}/{len(self.welcome_messages)} welcome messages") + return success_count + + def add_welcome_message(self, text: str, blocks: List[Dict] = None, channel: str = None) -> bool: + """Add a new welcome message to the configuration""" + try: + new_message = WelcomeMessage(text=text, blocks=blocks, channel=channel) + self.welcome_messages.append(new_message) + + # Save to file + messages_data = [] + for msg in self.welcome_messages: + msg_dict = {"text": msg.text} + if msg.blocks: + msg_dict["blocks"] = msg.blocks + if msg.channel: + msg_dict["channel"] = msg.channel + if msg.thread_ts: + msg_dict["thread_ts"] = msg.thread_ts + messages_data.append(msg_dict) + + self._save_welcome_messages(messages_data) + logger.info("Added new welcome message") + return True + except Exception as e: + logger.error(f"Failed to add welcome message: {e}") + return False + + async def start_monitoring(self, update_interval: int = 3600): + """ + Start monitoring and periodic updates + + Args: + update_interval: Interval in seconds between automatic updates + """ + self.running = True + logger.info(f"Starting Slack welcome agent with {update_interval}s interval") + + while self.running: + try: + await self.update_all_welcome_messages() + await asyncio.sleep(update_interval) + except KeyboardInterrupt: + logger.info("Received interrupt signal, stopping agent") + break + except Exception as e: + logger.error(f"Error in monitoring loop: {e}") + await asyncio.sleep(60) # Wait 1 minute before retrying + + def stop_monitoring(self): + """Stop the monitoring loop""" + self.running = False + logger.info("Stopping Slack welcome agent") + + def is_running(self) -> bool: + """Check if the agent is currently running""" + return self.running + + async def test_connection(self) -> bool: + """Test the Slack connection""" + try: + response = self.client.auth_test() + logger.info(f"Connected to Slack as {response['user']} in team {response['team']}") + return True + except SlackApiError as e: + logger.error(f"Slack connection failed: {e.response['error']}") + return False + except Exception as e: + logger.error(f"Connection test failed: {e}") + return False + +def main(): + """Main function to run the agent""" + # Load environment variables + slack_token = os.getenv('SLACK_BOT_TOKEN') + if not slack_token: + logger.error("SLACK_BOT_TOKEN environment variable not set") + return + + # Create and start the agent + agent = SlackWelcomeAgent(slack_token) + + # Test connection first + if not asyncio.run(agent.test_connection()): + logger.error("Failed to connect to Slack, exiting") + return + + try: + # Start monitoring + asyncio.run(agent.start_monitoring()) + except KeyboardInterrupt: + logger.info("Agent stopped by user") + except Exception as e: + logger.error(f"Agent crashed: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/welcome_config.json b/welcome_config.json new file mode 100644 index 0000000..72d4724 --- /dev/null +++ b/welcome_config.json @@ -0,0 +1,48 @@ +{ + "messages": [ + { + "text": "Welcome to Cursor for Slack! Here's how to use it:", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Welcome to Cursor for Slack!* :cursor:\n\nHere's how to get started:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "• Type `@Cursor help` for assistance\n• Use `@Cursor code` to get coding help\n• Try `@Cursor review` for code reviews\n• Say `@Cursor explain` to understand code\n• Ask `@Cursor debug` to troubleshoot issues" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Quick Commands:*\n`@Cursor help`\n`@Cursor code`\n`@Cursor review`" + }, + { + "type": "mrkdwn", + "text": "*Advanced Features:*\n`@Cursor explain`\n`@Cursor debug`\n`@Cursor optimize`" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "_Last updated: 2025-09-29 10:00:00 | Agent Status: Active_" + } + ] + } + ] + } + ] +} \ No newline at end of file