claude-mpm 4.2.24__py3-none-any.whl → 4.2.26__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 CHANGED
@@ -1 +1 @@
1
- 4.2.24
1
+ 4.2.26
@@ -399,6 +399,16 @@ def _execute_command(command: str, args) -> int:
399
399
  result = manage_mpm_init(args)
400
400
  return result if result is not None else 0
401
401
 
402
+ # Handle uninstall command with lazy import
403
+ if command == "uninstall":
404
+ # Lazy import to avoid loading unless needed
405
+ from .commands.uninstall import UninstallCommand
406
+
407
+ cmd = UninstallCommand()
408
+ result = cmd.execute(args)
409
+ # Convert CommandResult to exit code
410
+ return result.exit_code if result else 0
411
+
402
412
  # Map stable commands to their implementations
403
413
  command_map = {
404
414
  CLICommands.RUN.value: run_session,
@@ -93,7 +93,7 @@ class MonitorCommand(BaseCommand):
93
93
 
94
94
  # Get force restart flag
95
95
  force_restart = getattr(args, "force", False)
96
-
96
+
97
97
  # Check if already running
98
98
  if self.daemon.lifecycle.is_running() and not force_restart:
99
99
  existing_pid = self.daemon.lifecycle.get_pid()
@@ -112,27 +112,29 @@ class MonitorCommand(BaseCommand):
112
112
  if daemon_mode:
113
113
  # Give it a moment to fully initialize
114
114
  import time
115
+
115
116
  time.sleep(0.5)
116
-
117
+
117
118
  # Check if it's actually running
118
119
  if not self.daemon.lifecycle.is_running():
119
120
  return CommandResult.error_result(
120
121
  "Monitor daemon failed to start. Check ~/.claude-mpm/monitor-daemon.log for details."
121
122
  )
122
-
123
+
123
124
  # Get the actual PID
124
125
  actual_pid = self.daemon.lifecycle.get_pid()
125
126
  mode_info = f" in background (PID: {actual_pid})"
126
127
  else:
127
128
  mode_info = " in foreground"
128
-
129
+
129
130
  return CommandResult.success_result(
130
131
  f"Unified monitor daemon started on {host}:{port}{mode_info}",
131
132
  data={"url": f"http://{host}:{port}", "port": port, "mode": mode_str},
132
133
  )
133
-
134
+
134
135
  # Check if error was due to port already in use
135
136
  import socket
137
+
136
138
  try:
137
139
  test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
138
140
  test_sock.connect((host, port))
@@ -142,14 +144,14 @@ class MonitorCommand(BaseCommand):
142
144
  )
143
145
  except:
144
146
  pass
145
-
147
+
146
148
  return CommandResult.error_result(
147
149
  "Failed to start unified monitor daemon. Check ~/.claude-mpm/monitor-daemon.log for details."
148
150
  )
149
151
 
150
152
  def _stop_monitor(self, args) -> CommandResult:
151
153
  """Stop the unified monitor daemon."""
152
- self.logger.info("Stopping unified monitor daemon")
154
+ # Don't log here - the daemon will log when it stops
153
155
 
154
156
  # Get parameters from args or use defaults
155
157
  port = getattr(args, "port", None)
@@ -0,0 +1,178 @@
1
+ """
2
+ Uninstall command for claude-mpm CLI.
3
+
4
+ WHY: Users need a straightforward way to cleanly uninstall Claude MPM hooks
5
+ and other components without navigating through configuration menus.
6
+
7
+ DESIGN DECISIONS:
8
+ - Provide clear feedback about what is being removed
9
+ - Preserve user's other Claude settings
10
+ - Support both interactive confirmation and --yes flag
11
+ - Allow selective uninstallation of components
12
+ """
13
+
14
+ from typing import Optional
15
+
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.prompt import Confirm
19
+
20
+ from ...services.hook_installer_service import HookInstallerService
21
+ from ...utils.console import console as default_console
22
+ from ..shared import BaseCommand, CommandResult
23
+
24
+
25
+ class UninstallCommand(BaseCommand):
26
+ """Handle uninstallation of Claude MPM components."""
27
+
28
+ def __init__(self, console: Optional[Console] = None):
29
+ """Initialize the uninstall command.
30
+
31
+ Args:
32
+ console: Optional Rich console for output.
33
+ """
34
+ super().__init__("uninstall")
35
+ self.console = console or default_console
36
+ self.hook_service = HookInstallerService()
37
+
38
+ def run(self, args) -> CommandResult:
39
+ """Execute the uninstall command.
40
+
41
+ Args:
42
+ args: Parsed command line arguments.
43
+
44
+ Returns:
45
+ CommandResult indicating success or failure.
46
+ """
47
+ try:
48
+ # Check what component to uninstall
49
+ if args.component == "hooks" or args.all:
50
+ return self._uninstall_hooks(args)
51
+ if args.component == "all":
52
+ return self._uninstall_all(args)
53
+ # Default to hooks if no component specified
54
+ return self._uninstall_hooks(args)
55
+
56
+ except Exception as e:
57
+ self.console.print(f"[red]Error during uninstallation: {e}[/red]")
58
+ return CommandResult.error_result(str(e))
59
+
60
+ def _uninstall_hooks(self, args) -> CommandResult:
61
+ """Uninstall Claude MPM hooks.
62
+
63
+ Args:
64
+ args: Parsed command line arguments.
65
+
66
+ Returns:
67
+ CommandResult indicating success or failure.
68
+ """
69
+ try:
70
+ # Check if hooks are installed
71
+ if not self.hook_service.is_hooks_configured():
72
+ self.console.print(
73
+ "[yellow]No Claude MPM hooks are currently installed.[/yellow]"
74
+ )
75
+ return CommandResult.success_result("No hooks to uninstall")
76
+
77
+ # Get hook status for display
78
+ status = self.hook_service.get_hook_status()
79
+
80
+ # Show what will be removed
81
+ self.console.print(
82
+ "\n[cyan]The following Claude MPM hooks will be removed:[/cyan]"
83
+ )
84
+ for hook_type, configured in status.get("hook_types", {}).items():
85
+ if configured:
86
+ self.console.print(f" • {hook_type}")
87
+
88
+ # Confirm unless --yes flag is provided
89
+ if not args.yes:
90
+ if not Confirm.ask(
91
+ "\n[yellow]Do you want to proceed with uninstallation?[/yellow]"
92
+ ):
93
+ self.console.print("[yellow]Uninstallation cancelled.[/yellow]")
94
+ return CommandResult.success_result(
95
+ "Uninstallation cancelled by user"
96
+ )
97
+
98
+ # Perform uninstallation
99
+ self.console.print("\n[cyan]Uninstalling Claude MPM hooks...[/cyan]")
100
+ success = self.hook_service.uninstall_hooks()
101
+
102
+ if success:
103
+ self.console.print(
104
+ Panel(
105
+ "[green]✓ Claude MPM hooks have been successfully uninstalled.[/green]\n\n"
106
+ "Your other Claude settings have been preserved.",
107
+ title="Uninstallation Complete",
108
+ border_style="green",
109
+ )
110
+ )
111
+ return CommandResult.success_result("Hooks uninstalled successfully")
112
+ self.console.print(
113
+ "[red]Failed to uninstall hooks. Check the logs for details.[/red]"
114
+ )
115
+ return CommandResult.error_result("Failed to uninstall hooks")
116
+
117
+ except Exception as e:
118
+ return CommandResult.error_result(f"Error uninstalling hooks: {e}")
119
+
120
+ def _uninstall_all(self, args) -> CommandResult:
121
+ """Uninstall all Claude MPM components.
122
+
123
+ Args:
124
+ args: Parsed command line arguments.
125
+
126
+ Returns:
127
+ CommandResult indicating success or failure.
128
+ """
129
+ # For now, we only have hooks to uninstall
130
+ # This method can be extended in the future for other components
131
+ result = self._uninstall_hooks(args)
132
+
133
+ # Additional cleanup can be added here
134
+ # For example: removing agent configurations, cache, etc.
135
+
136
+ return result
137
+
138
+
139
+ def add_uninstall_parser(subparsers):
140
+ """Add the uninstall subparser.
141
+
142
+ Args:
143
+ subparsers: The subparsers object from the main parser.
144
+
145
+ Returns:
146
+ The configured uninstall parser.
147
+ """
148
+ parser = subparsers.add_parser(
149
+ "uninstall",
150
+ help="Uninstall Claude MPM components",
151
+ description="Remove Claude MPM hooks and other components while preserving other Claude settings",
152
+ )
153
+
154
+ # Component selection
155
+ parser.add_argument(
156
+ "component",
157
+ nargs="?",
158
+ choices=["hooks", "all"],
159
+ default="hooks",
160
+ help="Component to uninstall (default: hooks)",
161
+ )
162
+
163
+ # Confirmation bypass
164
+ parser.add_argument(
165
+ "-y", "--yes", action="store_true", help="Skip confirmation prompt"
166
+ )
167
+
168
+ # Force uninstall
169
+ parser.add_argument(
170
+ "--force", action="store_true", help="Force uninstallation even if errors occur"
171
+ )
172
+
173
+ # All components
174
+ parser.add_argument(
175
+ "--all", action="store_true", help="Uninstall all Claude MPM components"
176
+ )
177
+
178
+ return parser
@@ -345,6 +345,14 @@ def create_parser(
345
345
  except ImportError:
346
346
  pass
347
347
 
348
+ # Add uninstall command parser
349
+ try:
350
+ from ..commands.uninstall import add_uninstall_parser
351
+
352
+ add_uninstall_parser(subparsers)
353
+ except ImportError:
354
+ pass
355
+
348
356
  # Add debug command parser
349
357
  try:
350
358
  from .debug_parser import add_debug_subparser
@@ -61,9 +61,20 @@ set -e
61
61
  # Get the directory where this script is located
62
62
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63
63
 
64
- # Determine the claude-mpm root
65
- # The script is at src/claude_mpm/scripts/, so we go up 3 levels
66
- CLAUDE_MPM_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
64
+ # Determine the claude-mpm root based on installation type
65
+ # Check if we're in a pipx installation
66
+ if [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
67
+ # pipx installation - script is at lib/python*/site-packages/claude_mpm/scripts/
68
+ # The venv root is what we need for Python detection
69
+ CLAUDE_MPM_ROOT="$(echo "$SCRIPT_DIR" | sed 's|/lib/python.*/site-packages/.*||')"
70
+ elif [[ "$SCRIPT_DIR" == *"/site-packages/claude_mpm/scripts"* ]]; then
71
+ # Regular pip installation - script is in site-packages
72
+ # Use the Python environment root
73
+ CLAUDE_MPM_ROOT="$(python3 -c 'import sys; print(sys.prefix)')"
74
+ else
75
+ # Development installation - script is at src/claude_mpm/scripts/, so we go up 3 levels
76
+ CLAUDE_MPM_ROOT="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")"
77
+ fi
67
78
 
68
79
  # Debug logging (can be enabled via environment variable)
69
80
  if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
@@ -102,7 +113,16 @@ fi
102
113
  # Absolute path to Python executable with claude-mpm dependencies
103
114
  #
104
115
  find_python_command() {
105
- # 1. Check for project-local virtual environment (common in development)
116
+ # 1. Check if we're in a pipx installation first
117
+ if [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
118
+ # pipx installation - use the pipx venv's Python directly
119
+ if [ -f "$CLAUDE_MPM_ROOT/bin/python" ]; then
120
+ echo "$CLAUDE_MPM_ROOT/bin/python"
121
+ return
122
+ fi
123
+ fi
124
+
125
+ # 2. Check for project-local virtual environment (common in development)
106
126
  if [ -f "$CLAUDE_MPM_ROOT/venv/bin/activate" ]; then
107
127
  source "$CLAUDE_MPM_ROOT/venv/bin/activate"
108
128
  echo "$CLAUDE_MPM_ROOT/venv/bin/python"
@@ -122,8 +142,14 @@ find_python_command() {
122
142
  # Set up Python command
123
143
  PYTHON_CMD=$(find_python_command)
124
144
 
125
- # Check if we're in a development environment (has src directory)
126
- if [ -d "$CLAUDE_MPM_ROOT/src" ]; then
145
+ # Check installation type and set PYTHONPATH accordingly
146
+ if [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
147
+ # pipx installation - claude_mpm is already in the venv's site-packages
148
+ # No need to modify PYTHONPATH
149
+ if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
150
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] pipx installation detected" >> /tmp/claude-mpm-hook.log
151
+ fi
152
+ elif [ -d "$CLAUDE_MPM_ROOT/src" ]; then
127
153
  # Development install - add src to PYTHONPATH
128
154
  export PYTHONPATH="$CLAUDE_MPM_ROOT/src:$PYTHONPATH"
129
155
 
@@ -131,7 +157,7 @@ if [ -d "$CLAUDE_MPM_ROOT/src" ]; then
131
157
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Development environment detected" >> /tmp/claude-mpm-hook.log
132
158
  fi
133
159
  else
134
- # Pip install - claude_mpm should be in site-packages
160
+ # Regular pip install - claude_mpm should be in site-packages
135
161
  # No need to modify PYTHONPATH
136
162
  if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
137
163
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Pip installation detected" >> /tmp/claude-mpm-hook.log
@@ -75,8 +75,11 @@ class UnifiedDashboardManager(IUnifiedDashboardManager):
75
75
  self._lock = threading.Lock()
76
76
 
77
77
  def start_dashboard(
78
- self, port: int = 8765, background: bool = False, open_browser: bool = True,
79
- force_restart: bool = False
78
+ self,
79
+ port: int = 8765,
80
+ background: bool = False,
81
+ open_browser: bool = True,
82
+ force_restart: bool = False,
80
83
  ) -> Tuple[bool, bool]:
81
84
  """
82
85
  Start the dashboard using unified daemon.
@@ -95,19 +98,23 @@ class UnifiedDashboardManager(IUnifiedDashboardManager):
95
98
  daemon = UnifiedMonitorDaemon(
96
99
  host="localhost", port=port, daemon_mode=background
97
100
  )
98
-
101
+
99
102
  # Check if it's our service running
100
103
  is_ours, pid = daemon.lifecycle.is_our_service("localhost")
101
-
104
+
102
105
  if is_ours and not force_restart:
103
106
  # Our service is already running, just open browser if needed
104
- self.logger.info(f"Our dashboard already running on port {port} (PID: {pid})")
107
+ self.logger.info(
108
+ f"Our dashboard already running on port {port} (PID: {pid})"
109
+ )
105
110
  browser_opened = False
106
111
  if open_browser:
107
112
  browser_opened = self.open_browser(self.get_dashboard_url(port))
108
113
  return True, browser_opened
109
- elif is_ours and force_restart:
110
- self.logger.info(f"Force restarting our dashboard on port {port} (PID: {pid})")
114
+ if is_ours and force_restart:
115
+ self.logger.info(
116
+ f"Force restarting our dashboard on port {port} (PID: {pid})"
117
+ )
111
118
  elif self.is_dashboard_running(port) and not force_restart:
112
119
  # Different service is using the port
113
120
  self.logger.warning(f"Port {port} is in use by a different service")