Skip to main content
Glama
claude_wrapper.py•11.2 kB
""" Claude-Code Session Wrapper Handles tmux-based communication with existing Claude-Code sessions. Manages session communication, input/output, and interactive prompts. """ import asyncio import logging import os import subprocess from datetime import datetime from typing import Dict, List, Optional logger = logging.getLogger(__name__) class ClaudeWrapper: """Wrapper for a single tmux session running Claude-Code.""" def __init__(self, session_id: str, working_dir: Optional[str] = None): self.session_id = session_id # Should match tmux session name (e.g., claude-web-app) self.working_dir = working_dir or os.getcwd() self.created_at = datetime.now() self.last_activity = datetime.now() async def send_message(self, message: str) -> Dict: """Send a message to the tmux session running Claude-Code.""" try: logger.info(f"Sending message to session {self.session_id}: {message[:100]}...") # Send message to tmux session (separate from Enter) result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, message], capture_output=True, text=True, check=False ) if result.returncode != 0: raise RuntimeError(f"Failed to send message to tmux session {self.session_id}: {result.stderr}") # Separately send Enter key to submit enter_result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, "Enter"], capture_output=True, text=True, check=False ) if enter_result.returncode != 0: raise RuntimeError(f"Failed to send Enter key to tmux session {self.session_id}: {enter_result.stderr}") self.last_activity = datetime.now() # Capture current output after allowing time for processing await asyncio.sleep(2.0) # Increased wait time for Claude to process current_output = await self._capture_output() return { "status": "sent", "immediate_response": current_output, "timestamp": self.last_activity.isoformat() } except Exception as e: logger.error(f"Error sending message to session {self.session_id}: {e}") raise async def get_logs(self, lines: int = 50, mobile_friendly: bool = False) -> List[str]: """Get recent log lines from tmux session.""" try: # Capture current tmux pane content result = subprocess.run( ["tmux", "capture-pane", "-t", self.session_id, "-p"], capture_output=True, text=True, check=False ) if result.returncode != 0: logger.warning(f"Failed to capture logs from tmux session {self.session_id}") return [] output_lines = result.stdout.strip().split('\n') if result.stdout.strip() else [] recent_lines = output_lines[-lines:] if output_lines else [] if mobile_friendly: # Mobile optimization: truncate long lines mobile_logs = [] for line in recent_lines: if len(line) > 80: mobile_logs.append(line[:77] + "...") else: mobile_logs.append(line) return mobile_logs return recent_lines except Exception as e: logger.error(f"Error getting logs from session {self.session_id}: {e}") return [] def format_logs_for_chatgpt(self, logs: List[str], max_lines: int = 10) -> str: """Format logs as a readable string for ChatGPT responses.""" if not logs: return "No recent activity in this session." # Take most recent logs and format for conversation recent_logs = logs[-max_lines:] formatted = "Recent session activity:\n```\n" for log in recent_logs: formatted += log + "\n" formatted += "```" return formatted async def get_status(self) -> Dict: """Get current tmux session status.""" # Check if tmux session exists try: result = subprocess.run( ["tmux", "has-session", "-t", self.session_id], capture_output=True, check=False ) is_active = result.returncode == 0 except Exception: is_active = False return { "session_id": self.session_id, "status": "active" if is_active else "terminated", "created_at": self.created_at.isoformat(), "last_activity": self.last_activity.isoformat(), "working_dir": self.working_dir } async def terminate(self) -> bool: """Terminate the tmux session.""" try: logger.info(f"Terminating tmux session {self.session_id}") # Try graceful termination first - send Ctrl+C result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, "C-c"], capture_output=True, text=True, check=False ) await asyncio.sleep(1) # Check if session still exists check_result = subprocess.run( ["tmux", "has-session", "-t", self.session_id], capture_output=True, check=False ) if check_result.returncode == 0: # Session still exists, force kill it kill_result = subprocess.run( ["tmux", "kill-session", "-t", self.session_id], capture_output=True, text=True, check=False ) if kill_result.returncode != 0: logger.warning(f"Failed to kill tmux session {self.session_id}: {kill_result.stderr}") return False logger.info(f"Tmux session {self.session_id} terminated") return True except Exception as e: logger.error(f"Error terminating tmux session {self.session_id}: {e}") return False async def check_for_prompts(self) -> Optional[str]: """Check if Claude-Code is waiting for user input by examining recent output.""" try: # Check if tmux session exists result = subprocess.run( ["tmux", "has-session", "-t", self.session_id], capture_output=True, check=False ) if result.returncode != 0: return None # Get recent output from tmux session output = await self._capture_output() if not output: return None # Check for common prompt patterns in the output import re patterns = [ r".*\[y/n\].*", # Yes/no prompts r".*\(y/N\).*", # Yes/no with default r".*Continue\?.*", # Continue prompts r".*Enter.*:", # Input prompts ] # Look at the last few lines for prompts lines = output.split('\n')[-3:] for line in lines: for pattern in patterns: if re.search(pattern, line, re.IGNORECASE): return line except Exception as e: logger.warning(f"Error checking for prompts in session {self.session_id}: {e}") return None async def press_key(self, key: str) -> bool: """ Press a single key (useful for [y/n] prompts). Args: key: The key to press (e.g., "y", "n", "Enter", "C-c" for Ctrl+C) """ try: result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, key], capture_output=True, text=True, check=False ) if result.returncode != 0: logger.error(f"Failed to press key in tmux session {self.session_id}: {result.stderr}") return False self.last_activity = datetime.now() return True except Exception as e: logger.error(f"Error pressing key in session {self.session_id}: {e}") return False async def respond_to_prompt(self, response: str, press_enter: bool = True) -> bool: """ Respond to an interactive prompt (like [y/n] or Enter password:). Args: response: The text or key to send (e.g., "y", "yes", "password123") press_enter: Whether to press Enter after sending the response (default: True) Set to False for single-key prompts like [y/n] """ try: # Check if tmux session exists result = subprocess.run( ["tmux", "has-session", "-t", self.session_id], capture_output=True, check=False ) if result.returncode != 0: return False # Send the response text/key result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, response], capture_output=True, text=True, check=False ) if result.returncode != 0: logger.error(f"Failed to send response to tmux session {self.session_id}: {result.stderr}") return False # Optionally press Enter (for text responses that need submission) if press_enter: enter_result = subprocess.run( ["tmux", "send-keys", "-t", self.session_id, "Enter"], capture_output=True, text=True, check=False ) if enter_result.returncode != 0: logger.error(f"Failed to send Enter key to tmux session {self.session_id}: {enter_result.stderr}") return False self.last_activity = datetime.now() return True except Exception as e: logger.error(f"Error responding to prompt in session {self.session_id}: {e}") return False async def _capture_output(self) -> str: """Capture current output from tmux session.""" try: result = subprocess.run( ["tmux", "capture-pane", "-t", self.session_id, "-p"], capture_output=True, text=True, check=False ) if result.returncode != 0: logger.warning(f"Failed to capture output from tmux session {self.session_id}") return "" return result.stdout.strip() except Exception as e: logger.error(f"Error capturing output from session {self.session_id}: {e}") return ""

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mostafa-drz/claude-code-mcp-controller'

If you have feedback or need assistance with the MCP directory API, please join our Discord server