From 09117f9c6281d2a246ff11f6a99bcb9531fab278 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Feb 2026 23:53:49 -0600 Subject: [PATCH] remove old code --- automation_daemon.py | 433 ------------------------------------------- 1 file changed, 433 deletions(-) delete mode 100755 automation_daemon.py diff --git a/automation_daemon.py b/automation_daemon.py deleted file mode 100755 index 88a00c7..0000000 --- a/automation_daemon.py +++ /dev/null @@ -1,433 +0,0 @@ -#!/usr/bin/env python3 -""" -Claude Desktop Automation Daemon - -Continuously running daemon that sends periodic messages to Claude Desktop. -Wake interval can be controlled via the companion MCP server. - -Features: -- Python-based timer loop (not launchd scheduling) -- Dynamic wake intervals via MCP control -- Matrix integration: wakes Claude when Matrix messages arrive -- Shared state file for IPC with MCP and Matrix monitor -- Defaults to 60 minutes if no MCP override -- 2-minute rate limiting for Matrix wakes -- Auto-recovery and error handling -""" - -import json -import logging -import subprocess -import time -import signal -import sys -import fcntl -import os -from datetime import datetime, timedelta -from pathlib import Path -from threading import Lock - -# Configuration -SCRIPT_DIR = Path(__file__).parent -PYTHON_SCRIPT_PATH = SCRIPT_DIR / "send_to_claude.py" -APPLESCRIPT_PATH = SCRIPT_DIR / "send_to_claude.scpt" # Kept as fallback -STATE_FILE = Path.home() / ".claude-automation-state.json" -LOG_FILE = Path("/tmp/claude-automation-daemon.log") - -DEFAULT_INTERVAL_MINUTES = 60 -MAX_RETRIES = 3 -RETRY_DELAY = 2 # seconds -MIN_MATRIX_WAKE_INTERVAL_SECONDS = 30 # 30 seconds between Matrix wakes - -# Setup logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(LOG_FILE), - logging.StreamHandler() - ] -) -logger = logging.getLogger(__name__) - -# Global state -state_lock = Lock() -shutdown_flag = False - - -class WakeupState: - """Manages shared state with MCP server""" - - def __init__(self, state_file: Path): - self.state_file = state_file - self.lock = Lock() - - def load(self) -> dict: - """Load state from JSON file""" - with self.lock: - if not self.state_file.exists(): - return self._default_state() - - try: - with open(self.state_file, 'r') as f: - state = json.load(f) - logger.debug(f"Loaded state: {state}") - return state - except Exception as e: - logger.error(f"Failed to load state: {e}") - return self._default_state() - - def save(self, state: dict): - """Save state to JSON file atomically with file locking""" - with self.lock: - try: - # Write to temp file first - temp_path = self.state_file.with_suffix('.tmp') - with open(temp_path, 'w') as f: - fcntl.flock(f.fileno(), fcntl.LOCK_EX) - json.dump(state, f, indent=2) - f.flush() - os.fsync(f.fileno()) - # Atomic rename - temp_path.rename(self.state_file) - logger.debug(f"Saved state atomically") - except Exception as e: - logger.error(f"Failed to save state: {e}") - - def get_next_wake_time(self) -> datetime: - """Get the next scheduled wake time""" - state = self.load() - - # Check if MCP set a specific wake time - if state.get('next_wake_timestamp'): - try: - return datetime.fromisoformat(state['next_wake_timestamp']) - except ValueError: - logger.warning("Invalid next_wake_timestamp, using interval") - - # Use interval (from MCP or default) - interval_minutes = state.get('interval_minutes', DEFAULT_INTERVAL_MINUTES) - return datetime.now() + timedelta(minutes=interval_minutes) - - def set_next_wake_time(self, wake_time: datetime): - """Set the next wake time (called after processing MCP updates)""" - state = self.load() - state['next_wake_timestamp'] = wake_time.isoformat() - state['last_wake'] = datetime.now().isoformat() - self.save(state) - - def clear_next_wake_override(self): - """Clear MCP-set wake time, reverting to interval""" - state = self.load() - state.pop('next_wake_timestamp', None) - self.save(state) - - def is_paused(self) -> bool: - """Check if automation is paused""" - state = self.load() - return state.get('paused', False) - - def has_matrix_wake_request(self) -> bool: - """Check if Matrix integration requested a wake""" - state = self.load() - return state.get('matrix_wake_requested', False) - - def get_unprocessed_matrix_count(self) -> int: - """Get count of unprocessed Matrix messages""" - state = self.load() - messages = state.get('matrix_messages', []) - return len([msg for msg in messages if not msg.get('processed', False)]) - - def get_unprocessed_invite_count(self) -> int: - """Get count of unprocessed Matrix invites""" - state = self.load() - invites = state.get('matrix_invites', []) - return len([inv for inv in invites if not inv.get('processed', False)]) - - def get_unprocessed_file_count(self) -> int: - """Get count of unprocessed Matrix files""" - state = self.load() - files = state.get('matrix_files', []) - return len([f for f in files if not f.get('processed', False)]) - - def clear_matrix_wake_request(self): - """Clear Matrix wake request flag""" - state = self.load() - state['matrix_wake_requested'] = False - self.save(state) - - def prune_old_messages(self, max_age_hours: int = 24): - """Remove old processed messages to prevent state file bloat""" - state = self.load() - cutoff = datetime.now() - timedelta(hours=max_age_hours) - - original_count = len(state.get('matrix_messages', [])) - state['matrix_messages'] = [ - msg for msg in state.get('matrix_messages', []) - if not msg.get('processed', False) or - datetime.fromisoformat(msg['timestamp']) > cutoff - ] - - # Also prune invites and files - state['matrix_invites'] = [ - inv for inv in state.get('matrix_invites', []) - if not inv.get('processed', False) or - datetime.fromisoformat(inv['timestamp']) > cutoff - ] - - state['matrix_files'] = [ - f for f in state.get('matrix_files', []) - if not f.get('processed', False) or - datetime.fromisoformat(f['timestamp']) > cutoff - ] - - pruned_count = original_count - len(state.get('matrix_messages', [])) - if pruned_count > 0: - logger.info(f"Pruned {pruned_count} old processed messages") - self.save(state) - - def _default_state(self) -> dict: - """Default state structure""" - return { - 'interval_minutes': DEFAULT_INTERVAL_MINUTES, - 'paused': False, - 'last_wake': None, - 'next_wake_timestamp': None, - 'matrix_messages': [], - 'matrix_invites': [], - 'matrix_files': [], - 'matrix_last_wake': None, - 'matrix_wake_requested': False, - } - - -def send_to_claude(message: str, retry_count: int = 0) -> bool: - """ - Send message to Claude Desktop via AppleScript automation (with CMD+R refresh). - - Args: - message: Message to send - retry_count: Current retry attempt - - Returns: - bool: True if successful - """ - try: - logger.info(f"Attempt {retry_count + 1}/{MAX_RETRIES}: Sending message (with refresh)") - - # Use Python script (includes CMD+R refresh + 10s wait) - result = subprocess.run( - [sys.executable, str(PYTHON_SCRIPT_PATH), message], - capture_output=True, - text=True, - timeout=30 # Increased to accommodate 10s refresh delay - ) - - if result.returncode == 0: - logger.info("✓ Message sent successfully") - return True - else: - logger.warning(f"Script failed: {result.stdout} {result.stderr}") - return False - - except subprocess.TimeoutExpired: - logger.error("Message send timed out (>30s)") - return False - except FileNotFoundError: - logger.error(f"Python script not found at {PYTHON_SCRIPT_PATH}") - return False - except Exception as e: - logger.error(f"Unexpected error: {e}") - return False - - -def generate_message(matrix_messages: int = 0, matrix_invites: int = 0, matrix_files: int = 0) -> str: - """Generate the message to send to Claude""" - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - if matrix_messages > 0 or matrix_invites > 0 or matrix_files > 0: - parts = [] - if matrix_messages > 0: - parts.append(f"{matrix_messages} new Matrix message(s)") - if matrix_files > 0: - parts.append(f"{matrix_files} file(s)") - if matrix_invites > 0: - parts.append(f"{matrix_invites} room invite(s)") - - activity = " and ".join(parts) if len(parts) <= 2 else ", ".join(parts[:-1]) + f", and {parts[-1]}" - - tools = "Use get_matrix_messages() to retrieve messages (auto-marks as processed)" - if matrix_invites > 0: - tools += ", list_matrix_invites() to see invites (auto-marks as processed), and join_matrix_room() to accept" - tools += ". Use next_wakeup() to adjust monitoring interval if needed." - - return f"[Matrix Wakeup: {timestamp} - you have {activity}. {tools}]" - else: - return ( - f"[Autonomous System Wakeup: {timestamp}]" - ) - - -def attempt_send_message(message: str) -> bool: - """Try sending message with retries""" - for attempt in range(MAX_RETRIES): - if send_to_claude(message, attempt): - return True - - if attempt < MAX_RETRIES - 1: - logger.warning(f"Retrying in {RETRY_DELAY} seconds...") - time.sleep(RETRY_DELAY) - - logger.error(f"Failed after {MAX_RETRIES} attempts") - return False - - -def signal_handler(signum, frame): - """Handle shutdown signals gracefully""" - global shutdown_flag - logger.info(f"Received signal {signum}, shutting down gracefully...") - shutdown_flag = True - - -def main_loop(): - """Main daemon loop""" - global shutdown_flag - - state = WakeupState(STATE_FILE) - - logger.info("=" * 60) - logger.info("Claude Desktop Automation Daemon Starting") - logger.info(f"State file: {STATE_FILE}") - logger.info(f"Default interval: {DEFAULT_INTERVAL_MINUTES} minutes") - logger.info("=" * 60) - - # Register signal handlers - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - - # Track scheduled wake time separately (not affected by Matrix wakes) - scheduled_wake_time = None - - while not shutdown_flag: - try: - # Check if paused - if state.is_paused(): - logger.info("Automation is paused, sleeping for 60 seconds...") - time.sleep(60) - continue - - # Check for Matrix wake request - matrix_wake = state.has_matrix_wake_request() - matrix_count = state.get_unprocessed_matrix_count() if matrix_wake else 0 - invite_count = state.get_unprocessed_invite_count() if matrix_wake else 0 - file_count = state.get_unprocessed_file_count() if matrix_wake else 0 - - if matrix_wake: - logger.info(f"Matrix wake requested ({matrix_count} messages, {file_count} files, {invite_count} invites)") - # Clear the flag immediately so we don't wake again - state.clear_matrix_wake_request() - - # Wake up for Matrix activity - logger.info("Waking Claude for Matrix activity") - - # Generate and send message - message = generate_message(matrix_messages=matrix_count, matrix_invites=invite_count, matrix_files=file_count) - success = attempt_send_message(message) - - if success: - logger.info("Matrix wake message sent") - - # Wait grace period for Claude to process messages - grace_period = 30 - logger.info(f"Waiting {grace_period}s for Claude to process...") - time.sleep(grace_period) - - # IMPORTANT: Matrix wakes do NOT affect scheduled wake time - # Continue to next iteration (will use existing scheduled_wake_time) - logger.info(f"Matrix wake complete, next scheduled wake still at {scheduled_wake_time}") - continue - - # Calculate next scheduled wake time if needed - if scheduled_wake_time is None or datetime.now() >= scheduled_wake_time: - # Either first run or scheduled wake time has passed - scheduled_wake_time = state.get_next_wake_time() - logger.info(f"Calculated new scheduled wake time: {scheduled_wake_time}") - - next_wake = scheduled_wake_time - now = datetime.now() - - # Calculate sleep duration - if next_wake > now: - sleep_seconds = (next_wake - now).total_seconds() - logger.info(f"Next wake at {next_wake.strftime('%Y-%m-%d %H:%M:%S')} (in {sleep_seconds/60:.1f} minutes)") - - # Sleep in 1-minute chunks to allow for quick shutdown and Matrix wakes - while sleep_seconds > 0 and not shutdown_flag: - # Check for Matrix wake requests during sleep - if state.has_matrix_wake_request(): - logger.info("Matrix wake request detected during sleep, breaking early") - break - - chunk = min(60, sleep_seconds) - time.sleep(chunk) - sleep_seconds -= chunk - - if shutdown_flag: - break - - # If we broke early due to Matrix wake, continue to handle it - if state.has_matrix_wake_request(): - continue - - # Wake up! - logger.info("Wake time reached, sending message to Claude") - - # Generate and send message - message = generate_message() - success = attempt_send_message(message) - - if success: - logger.info("Message sent, Claude may adjust next wake time via MCP") - - # Wait a grace period for MCP to update state (Claude might call next_wakeup()) - grace_period = 30 # seconds - logger.info(f"Waiting {grace_period}s for potential MCP updates...") - time.sleep(grace_period) - - # Check if MCP updated the state - updated_state = state.load() - if updated_state.get('next_wake_timestamp'): - logger.info(f"MCP set custom wake time: {updated_state['next_wake_timestamp']}") - else: - logger.info(f"No MCP override, using default interval: {DEFAULT_INTERVAL_MINUTES} min") - - else: - logger.warning("Message send failed, will retry next cycle") - - # Clear the specific timestamp override (if it was used) - state.clear_next_wake_override() - - # Prune old processed messages to prevent state file bloat - state.prune_old_messages(max_age_hours=24) - - # Reset scheduled_wake_time so it recalculates on next iteration - # This allows MCP updates to take effect - scheduled_wake_time = None - - except Exception as e: - logger.exception(f"Error in main loop: {e}") - # Sleep a bit before retrying to avoid tight error loops - time.sleep(60) - - logger.info("Daemon shutting down") - - -if __name__ == "__main__": - try: - main_loop() - except KeyboardInterrupt: - logger.info("Interrupted by user") - sys.exit(0) - except Exception as e: - logger.exception(f"Fatal error: {e}") - sys.exit(1)