claude-mpm 3.9.2__py3-none-any.whl → 3.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +13 -0
- claude_mpm/cli/__init__.py +3 -1
- claude_mpm/cli/commands/__init__.py +3 -1
- claude_mpm/cli/commands/cleanup.py +430 -0
- claude_mpm/cli/commands/run.py +86 -0
- claude_mpm/cli/parser.py +9 -0
- claude_mpm/config/paths.py +56 -6
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +27 -14
- claude_mpm/core/config.py +15 -0
- claude_mpm/core/session_manager.py +108 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +167 -105
- claude_mpm/utils/paths.py +112 -8
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/METADATA +1 -1
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/RECORD +20 -19
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.5.dist-info}/top_level.txt +0 -0
    
        claude_mpm/config/paths.py
    CHANGED
    
    | @@ -33,6 +33,7 @@ class ClaudeMPMPaths: | |
| 33 33 |  | 
| 34 34 | 
             
                _instance: Optional['ClaudeMPMPaths'] = None
         | 
| 35 35 | 
             
                _project_root: Optional[Path] = None
         | 
| 36 | 
            +
                _is_installed: bool = False
         | 
| 36 37 |  | 
| 37 38 | 
             
                def __new__(cls) -> 'ClaudeMPMPaths':
         | 
| 38 39 | 
             
                    """Singleton pattern to ensure single instance."""
         | 
| @@ -43,6 +44,7 @@ class ClaudeMPMPaths: | |
| 43 44 | 
             
                def __init__(self):
         | 
| 44 45 | 
             
                    """Initialize paths if not already done."""
         | 
| 45 46 | 
             
                    if self._project_root is None:
         | 
| 47 | 
            +
                        self._is_installed = False
         | 
| 46 48 | 
             
                        self._detect_project_root()
         | 
| 47 49 |  | 
| 48 50 | 
             
                def _detect_project_root(self) -> None:
         | 
| @@ -53,16 +55,29 @@ class ClaudeMPMPaths: | |
| 53 55 | 
             
                    1. Look for definitive project markers (pyproject.toml, setup.py)
         | 
| 54 56 | 
             
                    2. Look for combination of markers to ensure we're at the right level
         | 
| 55 57 | 
             
                    3. Walk up from current file location
         | 
| 58 | 
            +
                    4. Handle both development and installed environments
         | 
| 56 59 | 
             
                    """
         | 
| 57 60 | 
             
                    # Start from this file's location
         | 
| 58 61 | 
             
                    current = Path(__file__).resolve()
         | 
| 59 62 |  | 
| 60 | 
            -
                    #  | 
| 63 | 
            +
                    # Check if we're in an installed environment (site-packages)
         | 
| 64 | 
            +
                    # In pip/pipx installs, the package is directly in site-packages
         | 
| 65 | 
            +
                    if 'site-packages' in str(current) or 'dist-packages' in str(current):
         | 
| 66 | 
            +
                        # We're in an installed environment
         | 
| 67 | 
            +
                        # The claude_mpm package directory itself is the "root" for resources
         | 
| 68 | 
            +
                        import claude_mpm
         | 
| 69 | 
            +
                        self._project_root = Path(claude_mpm.__file__).parent
         | 
| 70 | 
            +
                        self._is_installed = True
         | 
| 71 | 
            +
                        logger.debug(f"Installed environment detected, using package dir: {self._project_root}")
         | 
| 72 | 
            +
                        return
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    # We're in a development environment, look for project markers
         | 
| 61 75 | 
             
                    for parent in current.parents:
         | 
| 62 76 | 
             
                        # Check for definitive project root indicators
         | 
| 63 77 | 
             
                        # Prioritize pyproject.toml and setup.py as they're only at root
         | 
| 64 78 | 
             
                        if (parent / 'pyproject.toml').exists() or (parent / 'setup.py').exists():
         | 
| 65 79 | 
             
                            self._project_root = parent
         | 
| 80 | 
            +
                            self._is_installed = False
         | 
| 66 81 | 
             
                            logger.debug(f"Project root detected at: {parent} (found pyproject.toml or setup.py)")
         | 
| 67 82 | 
             
                            return
         | 
| 68 83 |  | 
| @@ -70,6 +85,7 @@ class ClaudeMPMPaths: | |
| 70 85 | 
             
                        # This combination is more likely to be the real project root
         | 
| 71 86 | 
             
                        if (parent / '.git').exists() and (parent / 'VERSION').exists():
         | 
| 72 87 | 
             
                            self._project_root = parent
         | 
| 88 | 
            +
                            self._is_installed = False
         | 
| 73 89 | 
             
                            logger.debug(f"Project root detected at: {parent} (found .git and VERSION)")
         | 
| 74 90 | 
             
                            return
         | 
| 75 91 |  | 
| @@ -77,12 +93,14 @@ class ClaudeMPMPaths: | |
| 77 93 | 
             
                    for parent in current.parents:
         | 
| 78 94 | 
             
                        if parent.name == 'claude-mpm':
         | 
| 79 95 | 
             
                            self._project_root = parent
         | 
| 96 | 
            +
                            self._is_installed = False
         | 
| 80 97 | 
             
                            logger.debug(f"Project root detected at: {parent} (by directory name)")
         | 
| 81 98 | 
             
                            return
         | 
| 82 99 |  | 
| 83 100 | 
             
                    # Last resort fallback: 3 levels up from this file
         | 
| 84 101 | 
             
                    # paths.py is in src/claude_mpm/config/
         | 
| 85 102 | 
             
                    self._project_root = current.parent.parent.parent
         | 
| 103 | 
            +
                    self._is_installed = False
         | 
| 86 104 | 
             
                    logger.warning(f"Project root fallback to: {self._project_root}")
         | 
| 87 105 |  | 
| 88 106 | 
             
                @property
         | 
| @@ -95,16 +113,26 @@ class ClaudeMPMPaths: | |
| 95 113 | 
             
                @property
         | 
| 96 114 | 
             
                def src_dir(self) -> Path:
         | 
| 97 115 | 
             
                    """Get the src directory."""
         | 
| 116 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 117 | 
            +
                        # In installed environment, there's no src directory
         | 
| 118 | 
            +
                        # Return the package directory itself
         | 
| 119 | 
            +
                        return self.project_root.parent
         | 
| 98 120 | 
             
                    return self.project_root / "src"
         | 
| 99 121 |  | 
| 100 122 | 
             
                @property
         | 
| 101 123 | 
             
                def claude_mpm_dir(self) -> Path:
         | 
| 102 124 | 
             
                    """Get the main claude_mpm package directory."""
         | 
| 125 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 126 | 
            +
                        # In installed environment, project_root IS the claude_mpm directory
         | 
| 127 | 
            +
                        return self.project_root
         | 
| 103 128 | 
             
                    return self.src_dir / "claude_mpm"
         | 
| 104 129 |  | 
| 105 130 | 
             
                @property
         | 
| 106 131 | 
             
                def agents_dir(self) -> Path:
         | 
| 107 132 | 
             
                    """Get the agents directory."""
         | 
| 133 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 134 | 
            +
                        # In installed environment, agents is directly under the package
         | 
| 135 | 
            +
                        return self.project_root / "agents"
         | 
| 108 136 | 
             
                    return self.claude_mpm_dir / "agents"
         | 
| 109 137 |  | 
| 110 138 | 
             
                @property
         | 
| @@ -140,36 +168,58 @@ class ClaudeMPMPaths: | |
| 140 168 | 
             
                @property
         | 
| 141 169 | 
             
                def scripts_dir(self) -> Path:
         | 
| 142 170 | 
             
                    """Get the scripts directory."""
         | 
| 171 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 172 | 
            +
                        # In installed environment, scripts might be in a different location or not exist
         | 
| 173 | 
            +
                        # Return a path that won't cause issues but indicates it's not available
         | 
| 174 | 
            +
                        return Path.home() / '.claude-mpm' / 'scripts'
         | 
| 143 175 | 
             
                    return self.project_root / "scripts"
         | 
| 144 176 |  | 
| 145 177 | 
             
                @property
         | 
| 146 178 | 
             
                def tests_dir(self) -> Path:
         | 
| 147 179 | 
             
                    """Get the tests directory."""
         | 
| 180 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 181 | 
            +
                        # Tests aren't distributed with installed packages
         | 
| 182 | 
            +
                        return Path.home() / '.claude-mpm' / 'tests'
         | 
| 148 183 | 
             
                    return self.project_root / "tests"
         | 
| 149 184 |  | 
| 150 185 | 
             
                @property
         | 
| 151 186 | 
             
                def docs_dir(self) -> Path:
         | 
| 152 187 | 
             
                    """Get the documentation directory."""
         | 
| 188 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 189 | 
            +
                        # Docs might be installed separately or not at all
         | 
| 190 | 
            +
                        return Path.home() / '.claude-mpm' / 'docs'
         | 
| 153 191 | 
             
                    return self.project_root / "docs"
         | 
| 154 192 |  | 
| 155 193 | 
             
                @property
         | 
| 156 194 | 
             
                def logs_dir(self) -> Path:
         | 
| 157 195 | 
             
                    """Get the logs directory (creates if doesn't exist)."""
         | 
| 158 | 
            -
                     | 
| 159 | 
            -
             | 
| 196 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 197 | 
            +
                        # Use user's home directory for logs in installed environment
         | 
| 198 | 
            +
                        logs = Path.home() / '.claude-mpm' / 'logs'
         | 
| 199 | 
            +
                    else:
         | 
| 200 | 
            +
                        logs = self.project_root / "logs"
         | 
| 201 | 
            +
                    logs.mkdir(parents=True, exist_ok=True)
         | 
| 160 202 | 
             
                    return logs
         | 
| 161 203 |  | 
| 162 204 | 
             
                @property
         | 
| 163 205 | 
             
                def temp_dir(self) -> Path:
         | 
| 164 206 | 
             
                    """Get the temporary files directory (creates if doesn't exist)."""
         | 
| 165 | 
            -
                     | 
| 166 | 
            -
             | 
| 207 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 208 | 
            +
                        # Use user's home directory for temp files in installed environment
         | 
| 209 | 
            +
                        temp = Path.home() / '.claude-mpm' / '.tmp'
         | 
| 210 | 
            +
                    else:
         | 
| 211 | 
            +
                        temp = self.project_root / ".tmp"
         | 
| 212 | 
            +
                    temp.mkdir(parents=True, exist_ok=True)
         | 
| 167 213 | 
             
                    return temp
         | 
| 168 214 |  | 
| 169 215 | 
             
                @property
         | 
| 170 216 | 
             
                def claude_mpm_dir_hidden(self) -> Path:
         | 
| 171 217 | 
             
                    """Get the hidden .claude-mpm directory (creates if doesn't exist)."""
         | 
| 172 | 
            -
                     | 
| 218 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 219 | 
            +
                        # Use current working directory in installed environment
         | 
| 220 | 
            +
                        hidden = Path.cwd() / ".claude-mpm"
         | 
| 221 | 
            +
                    else:
         | 
| 222 | 
            +
                        hidden = self.project_root / ".claude-mpm"
         | 
| 173 223 | 
             
                    hidden.mkdir(exist_ok=True)
         | 
| 174 224 | 
             
                    return hidden
         | 
| 175 225 |  | 
    
        claude_mpm/constants.py
    CHANGED
    
    
    
        claude_mpm/core/claude_runner.py
    CHANGED
    
    | @@ -1160,6 +1160,12 @@ Use these agents to delegate specialized work via the Task tool. | |
| 1160 1160 | 
             
                        version = __version__
         | 
| 1161 1161 | 
             
                        method_used = "package_import"
         | 
| 1162 1162 | 
             
                        self.logger.debug(f"Version obtained via package import: {version}")
         | 
| 1163 | 
            +
                        # If version already includes build number (PEP 440 format), extract it
         | 
| 1164 | 
            +
                        if '+build.' in version:
         | 
| 1165 | 
            +
                            parts = version.split('+build.')
         | 
| 1166 | 
            +
                            version = parts[0]  # Base version without build
         | 
| 1167 | 
            +
                            build_number = int(parts[1]) if len(parts) > 1 else None
         | 
| 1168 | 
            +
                            self.logger.debug(f"Extracted base version: {version}, build: {build_number}")
         | 
| 1163 1169 | 
             
                    except ImportError as e:
         | 
| 1164 1170 | 
             
                        self.logger.debug(f"Package import failed: {e}")
         | 
| 1165 1171 | 
             
                    except Exception as e:
         | 
| @@ -1192,19 +1198,20 @@ Use these agents to delegate specialized work via the Task tool. | |
| 1192 1198 | 
             
                        except Exception as e:
         | 
| 1193 1199 | 
             
                            self.logger.warning(f"Failed to read VERSION file: {e}")
         | 
| 1194 1200 |  | 
| 1195 | 
            -
                    # Try to read build number
         | 
| 1196 | 
            -
                     | 
| 1197 | 
            -
                         | 
| 1198 | 
            -
             | 
| 1199 | 
            -
                             | 
| 1200 | 
            -
             | 
| 1201 | 
            -
             | 
| 1202 | 
            -
             | 
| 1203 | 
            -
                         | 
| 1204 | 
            -
             | 
| 1205 | 
            -
             | 
| 1206 | 
            -
                         | 
| 1207 | 
            -
             | 
| 1201 | 
            +
                    # Try to read build number (only if not already obtained from version string)
         | 
| 1202 | 
            +
                    if build_number is None:
         | 
| 1203 | 
            +
                        try:
         | 
| 1204 | 
            +
                            build_file = paths.project_root / "BUILD_NUMBER"
         | 
| 1205 | 
            +
                            if build_file.exists():
         | 
| 1206 | 
            +
                                build_content = build_file.read_text().strip()
         | 
| 1207 | 
            +
                                build_number = int(build_content)
         | 
| 1208 | 
            +
                                self.logger.debug(f"Build number obtained from file: {build_number}")
         | 
| 1209 | 
            +
                        except (ValueError, IOError) as e:
         | 
| 1210 | 
            +
                            self.logger.debug(f"Could not read BUILD_NUMBER: {e}")
         | 
| 1211 | 
            +
                            build_number = None
         | 
| 1212 | 
            +
                        except Exception as e:
         | 
| 1213 | 
            +
                            self.logger.debug(f"Unexpected error reading BUILD_NUMBER: {e}")
         | 
| 1214 | 
            +
                            build_number = None
         | 
| 1208 1215 |  | 
| 1209 1216 | 
             
                    # Log final result
         | 
| 1210 1217 | 
             
                    if version == "0.0.0":
         | 
| @@ -1215,8 +1222,14 @@ Use these agents to delegate specialized work via the Task tool. | |
| 1215 1222 | 
             
                        self.logger.debug(f"Final version: {version} (method: {method_used})")
         | 
| 1216 1223 |  | 
| 1217 1224 | 
             
                    # Format version with build number if available
         | 
| 1225 | 
            +
                    # For development: Use PEP 440 format (e.g., "3.9.5+build.275")
         | 
| 1226 | 
            +
                    # For UI/logging: Use dash format (e.g., "v3.9.5-build.275")
         | 
| 1227 | 
            +
                    # For PyPI releases: Use clean version (e.g., "3.9.5")
         | 
| 1228 | 
            +
                    
         | 
| 1229 | 
            +
                    # Determine formatting context (default to UI format for claude_runner)
         | 
| 1218 1230 | 
             
                    if build_number is not None:
         | 
| 1219 | 
            -
                         | 
| 1231 | 
            +
                        # UI/logging format with 'v' prefix and dash separator
         | 
| 1232 | 
            +
                        return f"v{version}-build.{build_number}"
         | 
| 1220 1233 | 
             
                    else:
         | 
| 1221 1234 | 
             
                        return f"v{version}"
         | 
| 1222 1235 |  | 
    
        claude_mpm/core/config.py
    CHANGED
    
    | @@ -281,6 +281,21 @@ class Config: | |
| 281 281 | 
             
                        # Task and issue tracking
         | 
| 282 282 | 
             
                        "enable_persistent_tracking": True,
         | 
| 283 283 | 
             
                        "fallback_tracking_method": "logging",  # Options: "logging", "file", "disabled"
         | 
| 284 | 
            +
                        # Memory management configuration
         | 
| 285 | 
            +
                        "memory_management": {
         | 
| 286 | 
            +
                            "enabled": True,
         | 
| 287 | 
            +
                            "claude_json_warning_threshold_kb": 500,  # Warn at 500KB
         | 
| 288 | 
            +
                            "claude_json_critical_threshold_kb": 1024,  # Critical at 1MB
         | 
| 289 | 
            +
                            "auto_archive_enabled": False,  # Don't auto-archive by default
         | 
| 290 | 
            +
                            "archive_retention_days": 90,  # Keep archives for 90 days
         | 
| 291 | 
            +
                            "session_retention_hours": 24,  # Keep active sessions for 24 hours
         | 
| 292 | 
            +
                            "conversation_retention_days": 30,  # Keep conversations for 30 days
         | 
| 293 | 
            +
                            "monitor_memory_usage": True,  # Monitor memory usage
         | 
| 294 | 
            +
                            "memory_usage_log_interval": 300,  # Log memory usage every 5 minutes
         | 
| 295 | 
            +
                            "max_memory_usage_mb": 2048,  # Warn if memory usage exceeds 2GB
         | 
| 296 | 
            +
                            "cleanup_on_startup": False,  # Don't auto-cleanup on startup
         | 
| 297 | 
            +
                            "compress_archives": True  # Compress archived files
         | 
| 298 | 
            +
                        },
         | 
| 284 299 | 
             
                        # Evaluation system - Phase 2 Mirascope integration
         | 
| 285 300 | 
             
                        "enable_evaluation": True,
         | 
| 286 301 | 
             
                        "evaluation_storage_path": str(ConfigPaths.get_user_config_dir() / "training"),
         | 
| @@ -1,9 +1,11 @@ | |
| 1 1 | 
             
            """Session ID management for Claude subprocess optimization."""
         | 
| 2 2 |  | 
| 3 3 | 
             
            import uuid
         | 
| 4 | 
            -
            from typing import Optional, Dict, Any
         | 
| 4 | 
            +
            from typing import Optional, Dict, Any, List
         | 
| 5 5 | 
             
            from datetime import datetime, timedelta
         | 
| 6 6 | 
             
            import json
         | 
| 7 | 
            +
            import shutil
         | 
| 8 | 
            +
            import gzip
         | 
| 7 9 | 
             
            from pathlib import Path
         | 
| 8 10 |  | 
| 9 11 | 
             
            from ..core.logger import get_logger
         | 
| @@ -95,11 +97,15 @@ class SessionManager: | |
| 95 97 | 
             
                        self.active_sessions[session_id]["last_used"] = datetime.now().isoformat()
         | 
| 96 98 | 
             
                        self._save_sessions()
         | 
| 97 99 |  | 
| 98 | 
            -
                def cleanup_old_sessions(self, max_age_hours: int = 24):
         | 
| 100 | 
            +
                def cleanup_old_sessions(self, max_age_hours: int = 24, archive: bool = True):
         | 
| 99 101 | 
             
                    """Remove sessions older than max_age_hours.
         | 
| 100 102 |  | 
| 103 | 
            +
                    WHY: We archive old sessions instead of just deleting them to preserve
         | 
| 104 | 
            +
                    conversation history while reducing active memory usage.
         | 
| 105 | 
            +
                    
         | 
| 101 106 | 
             
                    Args:
         | 
| 102 107 | 
             
                        max_age_hours: Maximum age in hours
         | 
| 108 | 
            +
                        archive: Whether to archive sessions before removing
         | 
| 103 109 | 
             
                    """
         | 
| 104 110 | 
             
                    now = datetime.now()
         | 
| 105 111 | 
             
                    max_age = timedelta(hours=max_age_hours)
         | 
| @@ -110,6 +116,10 @@ class SessionManager: | |
| 110 116 | 
             
                        if now - created > max_age:
         | 
| 111 117 | 
             
                            expired.append(session_id)
         | 
| 112 118 |  | 
| 119 | 
            +
                    # Archive sessions if requested
         | 
| 120 | 
            +
                    if archive and expired:
         | 
| 121 | 
            +
                        self._archive_sessions([self.active_sessions[sid] for sid in expired])
         | 
| 122 | 
            +
                    
         | 
| 113 123 | 
             
                    for session_id in expired:
         | 
| 114 124 | 
             
                        del self.active_sessions[session_id]
         | 
| 115 125 | 
             
                        logger.info(f"Cleaned up expired session: {session_id}")
         | 
| @@ -180,11 +190,105 @@ class SessionManager: | |
| 180 190 | 
             
                            with open(session_file, 'r') as f:
         | 
| 181 191 | 
             
                                self.active_sessions = json.load(f)
         | 
| 182 192 |  | 
| 183 | 
            -
                            # Clean up old sessions on load
         | 
| 184 | 
            -
                            self.cleanup_old_sessions()
         | 
| 193 | 
            +
                            # Clean up old sessions on load (archive by default)
         | 
| 194 | 
            +
                            self.cleanup_old_sessions(archive=True)
         | 
| 195 | 
            +
                            
         | 
| 196 | 
            +
                            # Also check and clean .claude.json if needed
         | 
| 197 | 
            +
                            self._check_claude_json_size()
         | 
| 185 198 | 
             
                        except Exception as e:
         | 
| 186 199 | 
             
                            logger.error(f"Failed to load sessions: {e}")
         | 
| 187 200 | 
             
                            self.active_sessions = {}
         | 
| 201 | 
            +
                
         | 
| 202 | 
            +
                def _archive_sessions(self, sessions: List[Dict[str, Any]]):
         | 
| 203 | 
            +
                    """Archive sessions to compressed files.
         | 
| 204 | 
            +
                    
         | 
| 205 | 
            +
                    WHY: Archiving preserves conversation history while reducing the size
         | 
| 206 | 
            +
                    of active memory files like .claude.json.
         | 
| 207 | 
            +
                    
         | 
| 208 | 
            +
                    Args:
         | 
| 209 | 
            +
                        sessions: List of session data dictionaries to archive
         | 
| 210 | 
            +
                    """
         | 
| 211 | 
            +
                    if not sessions:
         | 
| 212 | 
            +
                        return
         | 
| 213 | 
            +
                    
         | 
| 214 | 
            +
                    archive_dir = self.session_dir.parent / "archives" / "sessions"
         | 
| 215 | 
            +
                    archive_dir.mkdir(parents=True, exist_ok=True)
         | 
| 216 | 
            +
                    
         | 
| 217 | 
            +
                    # Create timestamped archive file
         | 
| 218 | 
            +
                    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         | 
| 219 | 
            +
                    archive_name = f"sessions_archive_{timestamp}.json.gz"
         | 
| 220 | 
            +
                    archive_path = archive_dir / archive_name
         | 
| 221 | 
            +
                    
         | 
| 222 | 
            +
                    try:
         | 
| 223 | 
            +
                        # Compress and save sessions
         | 
| 224 | 
            +
                        with gzip.open(archive_path, 'wt', encoding='utf-8') as f:
         | 
| 225 | 
            +
                            json.dump(sessions, f, indent=2)
         | 
| 226 | 
            +
                        
         | 
| 227 | 
            +
                        logger.info(f"Archived {len(sessions)} sessions to {archive_path}")
         | 
| 228 | 
            +
                    except Exception as e:
         | 
| 229 | 
            +
                        logger.error(f"Failed to archive sessions: {e}")
         | 
| 230 | 
            +
                
         | 
| 231 | 
            +
                def _check_claude_json_size(self):
         | 
| 232 | 
            +
                    """Check .claude.json size and suggest cleanup if needed.
         | 
| 233 | 
            +
                    
         | 
| 234 | 
            +
                    WHY: Large .claude.json files cause memory issues. This provides
         | 
| 235 | 
            +
                    proactive monitoring and suggestions for cleanup.
         | 
| 236 | 
            +
                    """
         | 
| 237 | 
            +
                    claude_json_path = Path.home() / ".claude.json"
         | 
| 238 | 
            +
                    
         | 
| 239 | 
            +
                    if not claude_json_path.exists():
         | 
| 240 | 
            +
                        return
         | 
| 241 | 
            +
                    
         | 
| 242 | 
            +
                    file_size = claude_json_path.stat().st_size
         | 
| 243 | 
            +
                    warning_threshold = 500 * 1024  # 500KB
         | 
| 244 | 
            +
                    
         | 
| 245 | 
            +
                    if file_size > warning_threshold:
         | 
| 246 | 
            +
                        size_mb = file_size / (1024 * 1024)
         | 
| 247 | 
            +
                        logger.warning(f".claude.json is {size_mb:.1f}MB - consider running 'claude-mpm cleanup-memory'")
         | 
| 248 | 
            +
                
         | 
| 249 | 
            +
                def archive_claude_json(self, keep_days: int = 30) -> bool:
         | 
| 250 | 
            +
                    """Archive old conversations from .claude.json.
         | 
| 251 | 
            +
                    
         | 
| 252 | 
            +
                    WHY: This is called by the cleanup command to reduce memory usage
         | 
| 253 | 
            +
                    while preserving conversation history.
         | 
| 254 | 
            +
                    
         | 
| 255 | 
            +
                    Args:
         | 
| 256 | 
            +
                        keep_days: Number of days of history to keep
         | 
| 257 | 
            +
                        
         | 
| 258 | 
            +
                    Returns:
         | 
| 259 | 
            +
                        True if successful, False otherwise
         | 
| 260 | 
            +
                    """
         | 
| 261 | 
            +
                    claude_json_path = Path.home() / ".claude.json"
         | 
| 262 | 
            +
                    
         | 
| 263 | 
            +
                    if not claude_json_path.exists():
         | 
| 264 | 
            +
                        logger.info("No .claude.json file to archive")
         | 
| 265 | 
            +
                        return True
         | 
| 266 | 
            +
                    
         | 
| 267 | 
            +
                    try:
         | 
| 268 | 
            +
                        # Create backup first
         | 
| 269 | 
            +
                        archive_dir = Path.home() / ".claude-mpm" / "archives"
         | 
| 270 | 
            +
                        archive_dir.mkdir(parents=True, exist_ok=True)
         | 
| 271 | 
            +
                        
         | 
| 272 | 
            +
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         | 
| 273 | 
            +
                        backup_name = f"claude_json_backup_{timestamp}.json.gz"
         | 
| 274 | 
            +
                        backup_path = archive_dir / backup_name
         | 
| 275 | 
            +
                        
         | 
| 276 | 
            +
                        # Compress and backup current file
         | 
| 277 | 
            +
                        with open(claude_json_path, 'rb') as f_in:
         | 
| 278 | 
            +
                            with gzip.open(backup_path, 'wb') as f_out:
         | 
| 279 | 
            +
                                shutil.copyfileobj(f_in, f_out)
         | 
| 280 | 
            +
                        
         | 
| 281 | 
            +
                        logger.info(f"Created backup at {backup_path}")
         | 
| 282 | 
            +
                        
         | 
| 283 | 
            +
                        # For now, we don't modify the original .claude.json
         | 
| 284 | 
            +
                        # as we don't know its exact structure.
         | 
| 285 | 
            +
                        # The cleanup command handles this.
         | 
| 286 | 
            +
                        
         | 
| 287 | 
            +
                        return True
         | 
| 288 | 
            +
                        
         | 
| 289 | 
            +
                    except Exception as e:
         | 
| 290 | 
            +
                        logger.error(f"Failed to archive .claude.json: {e}")
         | 
| 291 | 
            +
                        return False
         | 
| 188 292 |  | 
| 189 293 |  | 
| 190 294 | 
             
            class OrchestrationSession:
         |