From 2e2aab26d6392a400d8581f10aa5d94eac82cb82 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 04:32:16 +0000 Subject: [PATCH 1/5] Fix mesh command matching to work with or without leading slash Previously, commands configured with a leading "/" (like "/ping") in commands_config.json would not work when sent from mesh without the slash (as "ping"). The keyword command section was explicitly checking that configured commands didn't start with "/" before matching. This fix normalizes both the configured command and the incoming message by stripping any leading "/" before comparison. Now commands work consistently regardless of: - How they're configured (with or without "/" in config) - How they're sent from mesh (with or without "/") Example: "/ping" in config now works when sent as either "ping" or "/ping" --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index c6b0ff0..5c46d2b 100644 --- a/main.py +++ b/main.py @@ -807,8 +807,11 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): for c in commands_config.get("commands", []): cmd_text = c.get("command", "").lower() + # Normalize both by removing leading '/' for flexible matching + normalized_cmd = cmd_text.lstrip('/') + normalized_first = first_word.lstrip('/') - if cmd_text and not cmd_text.startswith("/") and cmd_text == first_word: + if normalized_cmd and normalized_cmd == normalized_first: if "ai_prompt" in c: prompt = c["ai_prompt"].replace("{user_input}", user_input) From 4fe07ccf9f1069c85f830b67863822c8982ef6c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 04:38:08 +0000 Subject: [PATCH 2/5] Fix critical bug in handle_command argument extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handle_command function receives args (text after the command) as its second parameter, but was incorrectly trying to remove the command length from it again. This caused user input to be corrupted. Example of the bug: - Input: "/ai test question" - cmd="/ai", full_text="test question" (already args only) - Old code: user_prompt = "test question"[3:] = "t question" ❌ - Fixed: user_prompt = "test question" ✓ This affected: - AI commands (/ai, /bot, /query, /data) - line 686 - Emergency commands (/emergency, /911) - line 702 - Config-based slash commands - line 740 Now all slash commands correctly receive the full user input. --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 5c46d2b..5a174f8 100644 --- a/main.py +++ b/main.py @@ -683,7 +683,7 @@ def handle_command(cmd, full_text, sender_id): now = datetime.now(timezone_obj) return f"Current time in {timezone_str}: {now.strftime('%Y-%m-%d %H:%M:%S')}" elif cmd in ["/ai", "/bot", "/query", "/data"]: - user_prompt = full_text[len(cmd):].strip() + user_prompt = full_text.strip() if AI_PROVIDER == "home_assistant" and HOME_ASSISTANT_ENABLE_PIN: if not pin_is_valid(user_prompt): return "Security code missing or invalid. Use 'PIN=XXXX'" @@ -699,7 +699,7 @@ def handle_command(cmd, full_text, sender_id): return f"Node {sn} GPS: {lat}, {lon} (time: {tstr})" elif cmd in ["/emergency", "/911"]: lat, lon, tstamp = get_node_location(sender_id) - user_msg = full_text[len(cmd):].strip() + user_msg = full_text.strip() send_emergency_notification(sender_id, user_msg, lat, lon, tstamp) log_message(sender_id, f"EMERGENCY TRIGGERED: {full_text}", is_emergency=True) return "🚨 Emergency alert sent. Stay safe." @@ -737,7 +737,7 @@ def handle_command(cmd, full_text, sender_id): normalized_config_cmd = config_cmd.lstrip('/') if normalized_config_cmd == normalized_cmd: if "ai_prompt" in c: - user_input = full_text[len(cmd):].strip() + user_input = full_text.strip() custom_text = c["ai_prompt"].replace("{user_input}", user_input) if AI_PROVIDER == "home_assistant" and HOME_ASSISTANT_ENABLE_PIN: if not pin_is_valid(custom_text): From 69f5b2de93f528e92512c6f6afa35d4c5ad099d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 04:41:52 +0000 Subject: [PATCH 3/5] Simplify command system by removing slash-specific handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the system had complex logic to handle commands with and without slashes, resulting in confusing behavior and bugs. This commit completely simplifies the approach: BEFORE: - Separate handling for "/command" vs "command" - Section 2: Slash commands (if text.startswith("/")) - Section 3: Keyword commands (explicitly excluded commands with "/") - Commands in config needed "/" to work via slash handler - Inconsistent behavior between mesh and other sources AFTER: - Single unified command matching system - ALL slashes are stripped and ignored during matching - Commands work identically whether sent as "ping" or "/ping" - Commands in config work whether stored as "ping" or "/ping" - Much simpler code flow with less duplication Changes: 1. handle_command() now normalizes cmd by stripping "/" at entry 2. All command checks updated: "/about" → "about", etc. 3. Removed separate slash command section entirely 4. Unified command matching tries built-in then config commands 5. "ai on/off" updated to work without requiring "/" 6. Help command now shows normalized command names Result: Commands just work consistently across all entry points, regardless of slash usage in config or user messages. --- main.py | 74 +++++++++++++++++++++++++++------------------------------ 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/main.py b/main.py index 5a174f8..823c64f 100644 --- a/main.py +++ b/main.py @@ -675,14 +675,14 @@ def route_message_text(user_message, channel_idx): # Revised Command Handler (Case-Insensitive) # ----------------------------- def handle_command(cmd, full_text, sender_id): - cmd = cmd.lower() + cmd = cmd.lower().lstrip('/') # Normalize by removing any leading slash dprint(f"handle_command => cmd='{cmd}', full_text='{full_text}', sender_id={sender_id}") - if cmd == "/about": + if cmd == "about": return "Meshtastic-Controller Off Grid Chat - By: WWW.NerdsCorp.NET" - elif cmd == "/time": + elif cmd == "time": now = datetime.now(timezone_obj) return f"Current time in {timezone_str}: {now.strftime('%Y-%m-%d %H:%M:%S')}" - elif cmd in ["/ai", "/bot", "/query", "/data"]: + elif cmd in ["ai", "bot", "query", "data"]: user_prompt = full_text.strip() if AI_PROVIDER == "home_assistant" and HOME_ASSISTANT_ENABLE_PIN: if not pin_is_valid(user_prompt): @@ -690,32 +690,32 @@ def handle_command(cmd, full_text, sender_id): user_prompt = strip_pin(user_prompt) ai_answer = get_ai_response(user_prompt) return ai_answer if ai_answer else "🤖 [No AI response]" - elif cmd == "/whereami": + elif cmd == "whereami": lat, lon, tstamp = get_node_location(sender_id) sn = get_node_shortname(sender_id) if lat is None or lon is None: return f"🤖 Sorry {sn}, I have no GPS fix for your node." tstr = str(tstamp) if tstamp else "Unknown" return f"Node {sn} GPS: {lat}, {lon} (time: {tstr})" - elif cmd in ["/emergency", "/911"]: + elif cmd in ["emergency", "911"]: lat, lon, tstamp = get_node_location(sender_id) user_msg = full_text.strip() send_emergency_notification(sender_id, user_msg, lat, lon, tstamp) log_message(sender_id, f"EMERGENCY TRIGGERED: {full_text}", is_emergency=True) return "🚨 Emergency alert sent. Stay safe." - elif cmd == "/test": + elif cmd == "test": sn = get_node_shortname(sender_id) return f"Hello {sn}! Received {LOCAL_LOCATION_STRING} by {AI_NODE_NAME}." - elif cmd == "/help": - built_in = ["/about", "/query", "/whereami", "/emergency", "/911", "/test", "/motd"] - custom_cmds = [c.get("command") for c in commands_config.get("commands",[])] + elif cmd == "help": + built_in = ["about", "query", "whereami", "emergency", "911", "test", "motd"] + custom_cmds = [c.get("command", "").lstrip('/') for c in commands_config.get("commands",[])] return "Commands:\n" + ", ".join(built_in + custom_cmds) - elif cmd == "/motd": + elif cmd == "motd": return motd_content - elif cmd == "/sms": + elif cmd == "sms": parts = full_text.split(" ", 2) if len(parts) < 3: - return "Invalid syntax. Use: /sms " + return "Invalid syntax. Use: sms " phone_number = parts[1] message_text = parts[2] try: @@ -777,41 +777,37 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): dprint(f"AI auto-disabled for channel {channel_idx} (timeout)") # ---------------------------- - # 1. /ai on | /ai off + # 1. ai on | ai off (special handling) # ---------------------------- - if text_lower.startswith("/ai"): - parts = text_lower.split() - if len(parts) == 2 and parts[1] == "on": + parts = text_lower.split() + first_word_normalized = parts[0].lstrip('/') if parts else "" + + if first_word_normalized == "ai" and len(parts) == 2: + if parts[1] == "on": active_ai_channels[channel_idx] = now return "🤖 AI enabled for this channel." - if len(parts) == 2 and parts[1] == "off": + if parts[1] == "off": active_ai_channels.pop(channel_idx, None) return "🤖 AI disabled for this channel." - return "Usage: /ai on | /ai off" + return "Usage: ai on | ai off" # ---------------------------- - # 2. Slash commands - # ---------------------------- - if text.startswith("/"): - parts = text.split(maxsplit=1) - cmd = parts[0] - args = parts[1] if len(parts) > 1 else "" - return handle_command(cmd, args, sender_id) - + # 2. Command matching (unified - no slash required) # ---------------------------- - # 3. Keyword commands - # ---------------------------- - parts = text_lower.split(maxsplit=1) - first_word = parts[0] if parts else "" + first_word = first_word_normalized # Already normalized above user_input = text.split(maxsplit=1)[1] if len(text.split()) > 1 else "" + # First, try built-in commands via handle_command + if first_word: + result = handle_command(first_word, user_input, sender_id) + if result is not None: + return result + + # Then, try config-based commands for c in commands_config.get("commands", []): - cmd_text = c.get("command", "").lower() - # Normalize both by removing leading '/' for flexible matching - normalized_cmd = cmd_text.lstrip('/') - normalized_first = first_word.lstrip('/') + cmd_text = c.get("command", "").lower().lstrip('/') # Normalize config command - if normalized_cmd and normalized_cmd == normalized_first: + if cmd_text and cmd_text == first_word: if "ai_prompt" in c: prompt = c["ai_prompt"].replace("{user_input}", user_input) @@ -831,13 +827,13 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): return "No configured response for this keyword." # ---------------------------- - # 4. Home Assistant routing + # 3. Home Assistant routing # ---------------------------- if HOME_ASSISTANT_ENABLED and channel_idx == HOME_ASSISTANT_CHANNEL_INDEX: return route_message_text(text, channel_idx) # ---------------------------- - # 5. AI fallback + # 4. AI fallback # ---------------------------- if is_direct: return get_ai_response(text) @@ -847,7 +843,7 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): return get_ai_response(text) # ---------------------------- - # 6. Silent ignore + # 5. Silent ignore # ---------------------------- return None From bf7b1722351ba8797325b8e66a1ac5b5c641b332 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 04:45:43 +0000 Subject: [PATCH 4/5] Remove duplicate command handling and clean up code structure Fixed issues found during code review: 1. REMOVED duplicate command definitions: - "ai", "bot", "query", "data" were hardcoded in handle_command AND defined in commands_config.json - Now only in config, avoiding conflicting behavior 2. REMOVED duplicate config command loop: - Config commands were checked in BOTH handle_command AND parse_incoming_text - The loop in parse_incoming_text was unreachable dead code - Now config commands only checked in parse_incoming_text where we have access to channel context (needed for AI channel auto-activation) 3. UPDATED help command list: - Added "time" and "sms" to built-in commands list - Was showing outdated list of built-in commands 4. CLARIFIED code structure: - handle_command: Only built-in commands (about, time, whereami, etc.) - parse_incoming_text: Config-based commands with full channel context Result: Cleaner separation of concerns, no duplicate logic, proper channel activation for AI commands from config. --- main.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index 823c64f..bb24c4e 100644 --- a/main.py +++ b/main.py @@ -682,14 +682,6 @@ def handle_command(cmd, full_text, sender_id): elif cmd == "time": now = datetime.now(timezone_obj) return f"Current time in {timezone_str}: {now.strftime('%Y-%m-%d %H:%M:%S')}" - elif cmd in ["ai", "bot", "query", "data"]: - user_prompt = full_text.strip() - if AI_PROVIDER == "home_assistant" and HOME_ASSISTANT_ENABLE_PIN: - if not pin_is_valid(user_prompt): - return "Security code missing or invalid. Use 'PIN=XXXX'" - user_prompt = strip_pin(user_prompt) - ai_answer = get_ai_response(user_prompt) - return ai_answer if ai_answer else "🤖 [No AI response]" elif cmd == "whereami": lat, lon, tstamp = get_node_location(sender_id) sn = get_node_shortname(sender_id) @@ -707,7 +699,7 @@ def handle_command(cmd, full_text, sender_id): sn = get_node_shortname(sender_id) return f"Hello {sn}! Received {LOCAL_LOCATION_STRING} by {AI_NODE_NAME}." elif cmd == "help": - built_in = ["about", "query", "whereami", "emergency", "911", "test", "motd"] + built_in = ["about", "time", "whereami", "emergency", "911", "test", "motd", "sms"] custom_cmds = [c.get("command", "").lstrip('/') for c in commands_config.get("commands",[])] return "Commands:\n" + ", ".join(built_in + custom_cmds) elif cmd == "motd": @@ -730,24 +722,7 @@ def handle_command(cmd, full_text, sender_id): except Exception as e: print(f"⚠️ Failed to send SMS: {e}") return "Failed to send SMS." - for c in commands_config.get("commands", []): - config_cmd = c.get("command", "").lower() - # Normalize both commands by removing leading '/' for comparison - normalized_cmd = cmd.lstrip('/') - normalized_config_cmd = config_cmd.lstrip('/') - if normalized_config_cmd == normalized_cmd: - if "ai_prompt" in c: - user_input = full_text.strip() - custom_text = c["ai_prompt"].replace("{user_input}", user_input) - if AI_PROVIDER == "home_assistant" and HOME_ASSISTANT_ENABLE_PIN: - if not pin_is_valid(custom_text): - return "Security code missing or invalid." - custom_text = strip_pin(custom_text) - ans = get_ai_response(custom_text) - return ans if ans else "🤖 [No AI response]" - elif "response" in c: - return c["response"] - return "No configured response for this command." + # No match in built-in commands return None def parse_incoming_text(text, sender_id, is_direct, channel_idx): @@ -816,6 +791,7 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): return "Security code missing or invalid." prompt = strip_pin(prompt) + # Auto-enable AI for channels when config command with ai_prompt is used if not is_direct: active_ai_channels[channel_idx] = now From c8b638207e6406d1425a1b69ebdb7a39a6d33089 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 04:48:33 +0000 Subject: [PATCH 5/5] Remove all slash handling completely - pure word-based commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed ALL references to "/" from the entire codebase: CODE CHANGES (main.py): - Removed .lstrip('/') from handle_command (line 678) - Removed .lstrip('/') from help command list (line 703) - Removed .lstrip('/') from parse_incoming_text first_word (line 758) - Removed .lstrip('/') from config command matching (line 782) - Removed all normalization variables and comments about slashes - Updated comment: "no slash required" → "word-based" CONFIG CHANGES (commands_config.json): - Removed "/" from ALL command names: - /ping → ping - /weather → weather - /hello → hello (also updated message text) - /funfact → funfact - /joke → joke - /define → define - /translate → translate - /calc → calc - /inspire → inspire - /date → date - /time → curtime (renamed to avoid conflict with built-in) - /random → random - /rock → rock - /sport → sport - /recipe → recipe RESULT: The system now works purely on word matching with ZERO slash logic. Users send commands as plain words: "ping", "ai test", "help", etc. No more slash normalization, no more confusion, just simple word matching. --- config/commands_config.json | 32 ++++++++++++++++---------------- main.py | 13 ++++++------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/config/commands_config.json b/config/commands_config.json index e956183..e742bfb 100644 --- a/config/commands_config.json +++ b/config/commands_config.json @@ -17,64 +17,64 @@ "ai_prompt": "{user_input}" }, { - "command": "/ping", + "command": "ping", "response": "Pong!" }, { - "command": "/weather", + "command": "weather", "response": "Currently, local weather is unknown. (This is a placeholder.)" }, { - "command": "/hello", - "response": "Hello! I'm a Nerdscorp AI bot. Type /help to see the available commands." + "command": "hello", + "response": "Hello! I'm a Nerdscorp AI bot. Type help to see the available commands." }, { - "command": "/funfact", + "command": "funfact", "ai_prompt": "Give me a fun fact about {user_input}" }, { - "command": "/joke", + "command": "joke", "response": "Here's a joke for you: Why was the math book sad? Because it had too many problems." }, { - "command": "/define", + "command": "define", "ai_prompt": "Define '{user_input}'" }, { - "command": "/translate", + "command": "translate", "response": "Please specify a language to translate to.", "ai_prompt": "{language} translation of '{user_input}'" }, { - "command": "/calc", + "command": "calc", "response": "What operation do you want to perform? (add, subtract, multiply, divide) " }, { - "command": "/inspire", + "command": "inspire", "response": "Here's a quote for inspiration: 'Believe you can and you're halfway there.' - Theodore Roosevelt" }, { - "command": "/date", + "command": "date", "response": "The current date is: {date}" }, { - "command": "/time", + "command": "curtime", "response": "The current time is: {time}" }, { - "command": "/random", + "command": "random", "ai_prompt": "I'll generate a random number between 1 and 100. " }, { - "command": "/rock", + "command": "rock", "response": "Rock on! Here's a rock song for you: {song}" }, { - "command": "/sport", + "command": "sport", "response": "What sport do you want to know about? (e.g., soccer, basketball, football)" }, { - "command": "/recipe", + "command": "recipe", "ai_prompt": "Give me a recipe for {dish} using {ingredients}." } ] diff --git a/main.py b/main.py index bb24c4e..f01e54c 100644 --- a/main.py +++ b/main.py @@ -675,7 +675,7 @@ def route_message_text(user_message, channel_idx): # Revised Command Handler (Case-Insensitive) # ----------------------------- def handle_command(cmd, full_text, sender_id): - cmd = cmd.lower().lstrip('/') # Normalize by removing any leading slash + cmd = cmd.lower() dprint(f"handle_command => cmd='{cmd}', full_text='{full_text}', sender_id={sender_id}") if cmd == "about": return "Meshtastic-Controller Off Grid Chat - By: WWW.NerdsCorp.NET" @@ -700,7 +700,7 @@ def handle_command(cmd, full_text, sender_id): return f"Hello {sn}! Received {LOCAL_LOCATION_STRING} by {AI_NODE_NAME}." elif cmd == "help": built_in = ["about", "time", "whereami", "emergency", "911", "test", "motd", "sms"] - custom_cmds = [c.get("command", "").lstrip('/') for c in commands_config.get("commands",[])] + custom_cmds = [c.get("command", "") for c in commands_config.get("commands",[])] return "Commands:\n" + ", ".join(built_in + custom_cmds) elif cmd == "motd": return motd_content @@ -755,9 +755,9 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): # 1. ai on | ai off (special handling) # ---------------------------- parts = text_lower.split() - first_word_normalized = parts[0].lstrip('/') if parts else "" + first_word = parts[0] if parts else "" - if first_word_normalized == "ai" and len(parts) == 2: + if first_word == "ai" and len(parts) == 2: if parts[1] == "on": active_ai_channels[channel_idx] = now return "🤖 AI enabled for this channel." @@ -767,9 +767,8 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): return "Usage: ai on | ai off" # ---------------------------- - # 2. Command matching (unified - no slash required) + # 2. Command matching (word-based) # ---------------------------- - first_word = first_word_normalized # Already normalized above user_input = text.split(maxsplit=1)[1] if len(text.split()) > 1 else "" # First, try built-in commands via handle_command @@ -780,7 +779,7 @@ def parse_incoming_text(text, sender_id, is_direct, channel_idx): # Then, try config-based commands for c in commands_config.get("commands", []): - cmd_text = c.get("command", "").lower().lstrip('/') # Normalize config command + cmd_text = c.get("command", "").lower() if cmd_text and cmd_text == first_word: if "ai_prompt" in c: