"""Slash command handler for direct bot control.""" from __future__ import annotations import argparse import json import logging import re import uuid from typing import Optional, Tuple from agent.manager import manager from agent.scheduler import scheduler from agent.task_runner import task_runner from orchestrator.agent import agent from orchestrator.tools import set_current_user, get_current_chat logger = logging.getLogger(__name__) def parse_command(text: str) -> Optional[Tuple[str, str]]: """ Parse a slash command from text. Returns (command, args) or None if not a command. """ text = text.strip() if not text.startswith("/"): return None parts = text.split(None, 1) cmd = parts[0].lower() args = parts[1] if len(parts) > 1 else "" return (cmd, args) async def handle_command(user_id: str, text: str) -> Optional[str]: """ Handle a slash command. Returns the reply or None if not a command. """ parsed = parse_command(text) if not parsed: return None cmd, args = parsed logger.info("Command: %s args=%r user=...%s", cmd, args[:50], user_id[-8:]) set_current_user(user_id) if cmd in ("/new", "/n"): return await _cmd_new(user_id, args) elif cmd in ("/status", "/list", "/ls", "/l"): return await _cmd_status(user_id) elif cmd in ("/close", "/c"): return await _cmd_close(user_id, args) elif cmd in ("/switch", "/s"): return await _cmd_switch(user_id, args) elif cmd == "/retry": return await _cmd_retry(user_id) elif cmd in ("/help", "/h", "/?"): return _cmd_help() elif cmd == "/direct": return _cmd_direct(user_id) elif cmd == "/smart": return _cmd_smart(user_id) elif cmd == "/tasks": return _cmd_tasks() elif cmd == "/shell": return await _cmd_shell(args) elif cmd == "/remind": return await _cmd_remind(args) elif cmd in ("/nodes", "/node"): return await _cmd_nodes(user_id, args) else: return None async def _cmd_new(user_id: str, args: str) -> str: """Create a new session.""" if not args: return "Usage: /new [initial_message] [--timeout N]\nExample: /new todo_app fix the bug --timeout 600" parser = argparse.ArgumentParser() parser.add_argument("working_dir", nargs="?", help="Project directory") parser.add_argument("rest", nargs="*", help="Initial message") parser.add_argument("--timeout", type=int, default=None, help="CC timeout in seconds") parser.add_argument("--idle", type=int, default=None, help="Idle timeout in seconds") try: parsed = parser.parse_args(args.split()) except SystemExit: return "Usage: /new [initial_message] [--timeout N] [--idle N]" if not parsed.working_dir: return "Error: project_dir is required" working_dir = parsed.working_dir initial_msg = " ".join(parsed.rest) if parsed.rest else None from orchestrator.tools import CreateConversationTool tool = CreateConversationTool() result = await tool._arun( working_dir=working_dir, initial_message=initial_msg, cc_timeout=parsed.timeout, idle_timeout=parsed.idle, ) try: data = json.loads(result) if "error" in data: return f"Error: {data['error']}" conv_id = data.get("conv_id", "") agent._active_conv[user_id] = conv_id cwd = data.get("working_dir", working_dir) chat_id = get_current_chat() if chat_id: from bot.feishu import send_card, send_text, build_sessions_card sessions = manager.list_sessions(user_id=user_id) mode = "Direct 🟒" if agent.get_passthrough(user_id) else "Smart βšͺ" card = build_sessions_card(sessions, conv_id, mode) await send_card(chat_id, "chat_id", card) if initial_msg and data.get("response"): await send_text(chat_id, "chat_id", data["response"]) return "" reply = f"βœ“ Created session `{conv_id}` in `{cwd}`" if parsed.timeout: reply += f" (timeout: {parsed.timeout}s)" if initial_msg: reply += f"\n\nSent: {initial_msg[:100]}..." return reply except Exception: return result async def _cmd_status(user_id: str) -> str: """Show status: sessions and current mode.""" sessions = manager.list_sessions(user_id=user_id) active = agent.get_active_conv(user_id) passthrough = agent.get_passthrough(user_id) mode = "Direct 🟒" if passthrough else "Smart βšͺ" chat_id = get_current_chat() if chat_id: from bot.feishu import send_card, build_sessions_card card = build_sessions_card(sessions, active, mode) await send_card(chat_id, "chat_id", card) return "" if not sessions: return "No active sessions." lines = ["**Your Sessions:**\n"] for i, s in enumerate(sessions, 1): marker = "β†’ " if s["conv_id"] == active else " " lines.append(f"{marker}{i}. `{s['conv_id']}` - `{s['cwd']}`") lines.append(f"\n**Mode:** {mode}") lines.append("Use `/switch ` to activate a session.") lines.append("Use `/direct` or `/smart` to change mode.") return "\n".join(lines) async def _cmd_close(user_id: str, args: str) -> str: """Close a session.""" sessions = manager.list_sessions(user_id=user_id) if not sessions: return "No sessions to close." if args: try: idx = int(args) - 1 if 0 <= idx < len(sessions): conv_id = sessions[idx]["conv_id"] else: return f"Invalid session number. Use 1-{len(sessions)}." except ValueError: conv_id = args.strip() else: conv_id = agent.get_active_conv(user_id) if not conv_id: return "No active session. Use `/close ` or `/close `." try: success = await manager.close(conv_id, user_id=user_id) if success: if agent.get_active_conv(user_id) == conv_id: agent._active_conv[user_id] = None return f"βœ“ Closed session `{conv_id}`" else: return f"Session `{conv_id}` not found." except PermissionError as e: return str(e) async def _cmd_switch(user_id: str, args: str) -> str: """Switch to a different session.""" sessions = manager.list_sessions(user_id=user_id) if not sessions: return "No sessions available." if not args: return "Usage: /switch \n" + await _cmd_status(user_id) try: idx = int(args) - 1 if 0 <= idx < len(sessions): conv_id = sessions[idx]["conv_id"] agent._active_conv[user_id] = conv_id return f"βœ“ Switched to session `{conv_id}` ({sessions[idx]['cwd']})" else: return f"Invalid session number. Use 1-{len(sessions)}." except ValueError: return f"Invalid number: {args}" async def _cmd_retry(user_id: str) -> str: """Retry the last message (placeholder - needs history tracking).""" return "Retry not yet implemented. Just send your message again." def _cmd_direct(user_id: str) -> str: """Enable direct mode - messages go straight to Claude Code.""" conv = agent.get_active_conv(user_id) if not conv: return "No active session. Use `/new` or `/switch` first." agent.set_passthrough(user_id, True) return f"βœ“ Direct mode ON. Messages go directly to session `{conv}`." def _cmd_smart(user_id: str) -> str: """Enable smart mode - messages go through LLM for routing.""" agent.set_passthrough(user_id, False) return "βœ“ Smart mode ON. Messages go through LLM for intelligent routing." def _cmd_tasks() -> str: """List background tasks.""" tasks = task_runner.list_tasks() if not tasks: return "No background tasks." lines = ["**Background Tasks:**\n"] for t in tasks: status_emoji = {"completed": "βœ…", "failed": "❌", "running": "⏳", "pending": "⏸️"}.get( t["status"], "❓" ) lines.append(f"{status_emoji} #{t['task_id']} - {t['description'][:50]} ({t['elapsed']}s)") return "\n".join(lines) async def _cmd_shell(args: str) -> str: """Execute a shell command directly.""" if not args: return "Usage: /shell \nExample: /shell git status" from orchestrator.tools import ShellTool, get_current_chat tool = ShellTool() result = await tool._arun(command=args) try: data = json.loads(result) if "error" in data: return f"❌ {data['error']}" output = [] if data.get("stdout"): output.append(data["stdout"]) if data.get("stderr"): output.append(f"[stderr] {data['stderr']}") output.append(f"[exit code: {data.get('exit_code', '?')}]") return "\n".join(output) if output else "(no output)" except json.JSONDecodeError: return result async def _cmd_remind(args: str) -> str: """Set a reminder.""" if not args: return "Usage: /remind