From 5d2d336d7fd9dec60997c7094dca1fa68bf13805 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Sep 2025 10:03:30 +0000 Subject: [PATCH] feat: Add Slack welcome message agent Co-authored-by: kaj.drobin --- .env | 8 + .env.example | 8 + README.md | 193 +++++++++++++ __pycache__/agent_controller.cpython-313.pyc | Bin 0 -> 11045 bytes agent_controller.py | 220 +++++++++++++++ quick_status.py | 51 ++++ requirements.txt | 3 + setup.sh | 39 +++ slack_welcome_agent.py | 271 +++++++++++++++++++ welcome_config.json | 48 ++++ 10 files changed, 841 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 README.md create mode 100644 __pycache__/agent_controller.cpython-313.pyc create mode 100755 agent_controller.py create mode 100644 quick_status.py create mode 100644 requirements.txt create mode 100755 setup.sh create mode 100755 slack_welcome_agent.py create mode 100644 welcome_config.json 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 0000000000000000000000000000000000000000..1fa5b3c773485dd5e13976ac31d64d61a0dd530d GIT binary patch literal 11045 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 literal 0 HcmV?d00001 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