diff --git a/config.example.yaml b/config.example.yaml index 165cfe3..a255db4 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,9 +6,8 @@ # === BOT IDENTITY === bot: - name: ai # Bot's trigger name (users say "@ai help") + name: ai # Bot's display name owner: "" # Owner's callsign (optional) - respond_to_mentions: true # Respond when name is mentioned respond_to_dms: true # Respond to direct messages filter_bbs_protocols: true # Ignore advBBS sync/notification messages @@ -19,12 +18,6 @@ connection: tcp_host: localhost # For TCP connection (meshtasticd) tcp_port: 4403 -# === CHANNEL FILTERING === -channels: - mode: all # all | whitelist - whitelist: # Only respond on these channels (if mode=whitelist) - - 0 - # === RESPONSE BEHAVIOR === response: delay_min: 2.2 # Min delay before responding (seconds) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 15dbab4..0313db3 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -15,7 +15,6 @@ if [ ! -f "$MESHAI_CONFIG" ]; then bot: name: ai owner: "" - respond_to_mentions: true respond_to_dms: true filter_bbs_protocols: true @@ -25,11 +24,6 @@ connection: tcp_host: localhost tcp_port: 4403 -channels: - mode: all - whitelist: - - 0 - response: delay_min: 2.2 delay_max: 3.0 @@ -74,38 +68,50 @@ ttyd -W -p 7682 \ /bin/bash -c 'while true; do python3 -m meshai --config-file "$MESHAI_CONFIG" --config; sleep 1; done' & # Keep ttyd running even if bot fails -trap "kill %1 2>/dev/null; kill %2 2>/dev/null" EXIT +trap "kill %1 2>/dev/null" EXIT -# Restart watcher - monitors for restart signal from config tool -BOT_PID_FILE="/tmp/meshai_bot.pid" -( - while true; do - if [ -f /tmp/meshai_restart ]; then - rm -f /tmp/meshai_restart - echo "Restart signal received, restarting bot..." - # Kill bot using PID file - if [ -f "$BOT_PID_FILE" ]; then - BOT_PID=$(cat "$BOT_PID_FILE") - if kill -0 "$BOT_PID" 2>/dev/null; then - kill "$BOT_PID" 2>/dev/null || true - echo "Sent TERM to bot (PID $BOT_PID)" - fi - fi - # Debounce - wait before checking for more signals - sleep 3 - fi - sleep 2 +# Kill bot gracefully with SIGKILL fallback +kill_bot() { + local pid=$1 + if ! kill -0 "$pid" 2>/dev/null; then + return + fi + kill "$pid" 2>/dev/null || true + echo "Sent SIGTERM to bot (PID $pid)" + # Wait up to 5 seconds for graceful shutdown + for i in 1 2 3 4 5; do + kill -0 "$pid" 2>/dev/null || return + sleep 1 done -) & + # Force kill if still alive + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + echo "Sent SIGKILL to bot (PID $pid)" + fi +} -# Start the bot in a loop - retry on failure +# Start the bot in a loop with integrated restart watcher echo "Starting MeshAI..." +rm -f /tmp/meshai_restart while true; do python -m meshai --config-file "$MESHAI_CONFIG" & BOT_PID=$! - echo "$BOT_PID" > "$BOT_PID_FILE" - wait $BOT_PID || true - rm -f "$BOT_PID_FILE" - echo "Bot exited. Check config at http://localhost:7682. Retrying in 5s..." - sleep 5 + echo "$BOT_PID" > /tmp/meshai.pid + echo "Bot started (PID $BOT_PID)" + + # Poll: wait for bot to exit OR restart signal + while kill -0 $BOT_PID 2>/dev/null; do + if [ -f /tmp/meshai_restart ]; then + rm -f /tmp/meshai_restart + echo "Restart signal received, restarting bot..." + kill_bot $BOT_PID + break + fi + sleep 1 + done + + wait $BOT_PID 2>/dev/null || true + rm -f /tmp/meshai.pid + echo "Bot exited. Restarting in 3s..." + sleep 3 done diff --git a/meshai/cli/configurator.py b/meshai/cli/configurator.py index 46a7ca1..33471a9 100644 --- a/meshai/cli/configurator.py +++ b/meshai/cli/configurator.py @@ -69,15 +69,14 @@ class Configurator: disabled_count = len(self.config.commands.disabled_commands) cmd_status = f"{disabled_count} disabled" if disabled_count else "all enabled" - table.add_row("1", "Bot Settings", f"@{self.config.bot.name}") + table.add_row("1", "Bot Settings", self.config.bot.name) table.add_row("2", "Connection", f"{self.config.connection.type}") table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}") table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max") - table.add_row("5", "Channels", f"{self.config.channels.mode}") - table.add_row("6", "History & Memory", f"{self.config.history.max_messages_per_user} msgs") - table.add_row("7", "Commands", cmd_status) - table.add_row("8", "Weather", f"{self.config.weather.primary}") - table.add_row("9", "Setup Wizard", "[dim]First-time setup[/dim]") + table.add_row("5", "History & Memory", f"{self.config.history.max_messages_per_user} msgs") + table.add_row("6", "Commands", cmd_status) + table.add_row("7", "Weather", f"{self.config.weather.primary}") + table.add_row("8", "Setup Wizard", "[dim]First-time setup[/dim]") console.print(table) console.print() @@ -86,13 +85,13 @@ class Configurator: if self.modified: console.print("[yellow]* Unsaved changes[/yellow]") console.print() - console.print("[white]10. Save[/white] [dim]Save config, stay in menu[/dim]") - console.print("[green]11. Save & Restart Bot[/green] [dim]Apply changes now[/dim]") - console.print("[white]12. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]") - console.print("[white]13. Exit without Saving[/white]") + console.print("[white] 9. Save[/white] [dim]Save config, stay in menu[/dim]") + console.print("[green]10. Save & Restart Bot[/green] [dim]Apply changes now[/dim]") + console.print("[white]11. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]") + console.print("[white]12. Exit without Saving[/white]") console.print() - choice = IntPrompt.ask("Select option", default=11) + choice = IntPrompt.ask("Select option", default=10) if choice == 1: self._bot_settings() @@ -103,23 +102,21 @@ class Configurator: elif choice == 4: self._response_settings() elif choice == 5: - self._channel_settings() - elif choice == 6: self._history_settings() - elif choice == 7: + elif choice == 6: self._command_settings() - elif choice == 8: + elif choice == 7: self._weather_settings() - elif choice == 9: + elif choice == 8: self._setup_wizard() - elif choice == 10: + elif choice == 9: self._save_only() - elif choice == 11: + elif choice == 10: self._save_and_restart() - elif choice == 12: + elif choice == 11: self._save_restart_exit() break - elif choice == 13: + elif choice == 12: break def _show_header(self) -> None: @@ -148,18 +145,13 @@ class Configurator: table.add_column("Setting", style="white") table.add_column("Value", style="green") - table.add_row("1", "Bot Name (@mention)", self.config.bot.name) + table.add_row("1", "Bot Name", self.config.bot.name) table.add_row("2", "Owner", self.config.bot.owner or "[dim]not set[/dim]") table.add_row( - "3", - "Respond to @mentions", - self._status_icon(self.config.bot.respond_to_mentions), + "3", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms) ) table.add_row( - "4", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms) - ) - table.add_row( - "5", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols) + "4", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols) ) table.add_row("0", "Back", "") @@ -181,18 +173,11 @@ class Configurator: self.config.bot.owner = value self.modified = True elif choice == 3: - value = Confirm.ask( - "Respond to @mentions?", default=self.config.bot.respond_to_mentions - ) - if value != self.config.bot.respond_to_mentions: - self.config.bot.respond_to_mentions = value - self.modified = True - elif choice == 4: value = Confirm.ask("Respond to DMs?", default=self.config.bot.respond_to_dms) if value != self.config.bot.respond_to_dms: self.config.bot.respond_to_dms = value self.modified = True - elif choice == 5: + elif choice == 4: value = Confirm.ask("Filter BBS protocols?", default=self.config.bot.filter_bbs_protocols) if value != self.config.bot.filter_bbs_protocols: self.config.bot.filter_bbs_protocols = value @@ -478,49 +463,6 @@ class Configurator: self.config.response.max_messages = value self.modified = True - def _channel_settings(self) -> None: - """Channel filtering settings submenu.""" - while True: - self._clear() - console.print("[bold]Channel Filtering[/bold]\n") - - table = Table(box=box.ROUNDED) - table.add_column("Option", style="cyan", width=4) - table.add_column("Setting", style="white") - table.add_column("Value", style="green") - - whitelist_str = ", ".join(str(c) for c in self.config.channels.whitelist) - table.add_row("1", "Mode", self.config.channels.mode) - table.add_row("2", "Whitelist Channels", whitelist_str or "[dim]none[/dim]") - table.add_row("0", "Back", "") - - console.print(table) - console.print() - - choice = IntPrompt.ask("Select option", default=0) - - if choice == 0: - return - elif choice == 1: - console.print("\n[cyan]1.[/cyan] all - Respond on all channels") - console.print("[cyan]2.[/cyan] whitelist - Only respond on specific channels") - sel = IntPrompt.ask("Select", default=1 if self.config.channels.mode == "all" else 2) - value = "all" if sel == 1 else "whitelist" - if value != self.config.channels.mode: - self.config.channels.mode = value - self.modified = True - elif choice == 2: - value = Prompt.ask( - "Whitelist (comma-separated)", default=whitelist_str - ) - try: - channels = [int(c.strip()) for c in value.split(",") if c.strip()] - if channels != self.config.channels.whitelist: - self.config.channels.whitelist = channels - self.modified = True - except ValueError: - console.print("[red]Invalid input. Use comma-separated numbers.[/red]") - def _history_settings(self) -> None: """History settings submenu.""" while True: @@ -603,7 +545,7 @@ class Configurator: # Step 1: Bot identity console.print("[bold cyan]Step 1: Bot Identity[/bold cyan]") - self.config.bot.name = Prompt.ask("Bot name (for @mentions)", default="ai") + self.config.bot.name = Prompt.ask("Bot name", default="ai") self.config.bot.owner = Prompt.ask("Your name/callsign", default="") console.print() diff --git a/meshai/config.py b/meshai/config.py index ffeb5b4..d9e491e 100644 --- a/meshai/config.py +++ b/meshai/config.py @@ -17,7 +17,6 @@ class BotConfig: name: str = "ai" owner: str = "" - respond_to_mentions: bool = True respond_to_dms: bool = True filter_bbs_protocols: bool = True @@ -32,14 +31,6 @@ class ConnectionConfig: tcp_port: int = 4403 -@dataclass -class ChannelsConfig: - """Channel filtering settings.""" - - mode: str = "all" # all or whitelist - whitelist: list[int] = field(default_factory=lambda: [0]) - - @dataclass class ResponseConfig: """Response behavior settings.""" @@ -134,7 +125,6 @@ class Config: bot: BotConfig = field(default_factory=BotConfig) connection: ConnectionConfig = field(default_factory=ConnectionConfig) - channels: ChannelsConfig = field(default_factory=ChannelsConfig) response: ResponseConfig = field(default_factory=ResponseConfig) history: HistoryConfig = field(default_factory=HistoryConfig) memory: MemoryConfig = field(default_factory=MemoryConfig) diff --git a/meshai/main.py b/meshai/main.py index 08c4230..e04bf4f 100644 --- a/meshai/main.py +++ b/meshai/main.py @@ -174,22 +174,12 @@ class MeshAI: if not response: return - # Send response - if message.is_dm: - await self.responder.send_response( - text=response, - destination=message.sender_id, - channel=message.channel, - ) - else: - formatted = self.responder.format_channel_response( - response, message.sender_name, mention_sender=True - ) - await self.responder.send_response( - text=formatted, - destination=None, - channel=message.channel, - ) + # Send DM response + await self.responder.send_response( + text=response, + destination=message.sender_id, + channel=message.channel, + ) except Exception as e: logger.error(f"Error handling message: {e}", exc_info=True) diff --git a/meshai/responder.py b/meshai/responder.py index c004374..4b03995 100644 --- a/meshai/responder.py +++ b/meshai/responder.py @@ -137,24 +137,3 @@ class Responder: return pos return 0 - - def format_channel_response( - self, text: str, sender_name: str, mention_sender: bool = False - ) -> str: - """Format response for channel context. - - Args: - text: Response text - sender_name: Name of sender being replied to - mention_sender: Whether to prefix with sender's name - - Returns: - Formatted response - """ - if mention_sender: - # Check if adding prefix would exceed max length - prefix = f"@{sender_name}: " - if len(prefix) + len(text) <= self.config.max_length * self.config.max_messages: - return prefix + text - - return text diff --git a/meshai/router.py b/meshai/router.py index e496211..018071b 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -68,13 +68,13 @@ class MessageRouter: self.dispatcher = dispatcher self.llm = llm_backend - # Compile mention pattern - bot_name = re.escape(config.bot.name) - self._mention_pattern = re.compile(rf"@{bot_name}\b", re.IGNORECASE) def should_respond(self, message: MeshMessage) -> bool: """Determine if we should respond to this message. + DM-only bot: ignores all public channel messages. + Commands and conversational LLM responses both work in DMs. + Args: message: Incoming message @@ -85,35 +85,20 @@ class MessageRouter: if message.sender_id == self.connector.my_node_id: return False + # Only respond to DMs + if not message.is_dm: + return False + + if not self.config.bot.respond_to_dms: + return False + # Ignore advBBS protocol and notification messages if self.config.bot.filter_bbs_protocols: if any(message.text.startswith(p) for p in ADVBBS_PREFIXES): logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") return False - # Check if DM — conversational mode only, skip !commands - # (let MeshMonitor or other bots handle bang commands in DMs) - if message.is_dm: - if self.dispatcher.is_command(message.text): - return False - return self.config.bot.respond_to_dms - - # Check channel filtering - if self.config.channels.mode == "whitelist": - if message.channel not in self.config.channels.whitelist: - return False - - # Check for @mention - if self.config.bot.respond_to_mentions: - if self._mention_pattern.search(message.text): - return True - - # Check for bang command (always respond to commands) - if self.dispatcher.is_command(message.text): - return True - - # Not a DM, no mention, no command - ignore - return False + return True async def route(self, message: MeshMessage) -> RouteResult: """Route a message and generate response. @@ -200,11 +185,8 @@ class MessageRouter: logger.debug(f"Persisted summary for {user_id}") def _clean_query(self, text: str) -> str: - """Remove @mention and check for prompt injection.""" - # Remove @botname mention - cleaned = self._mention_pattern.sub("", text) - # Clean up extra whitespace - cleaned = " ".join(cleaned.split()) + """Clean up query text and check for prompt injection.""" + cleaned = " ".join(text.split()) cleaned = cleaned.strip() # Check for prompt injection