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 +1 -1
- claude_mpm/cli/__init__.py +10 -0
- claude_mpm/cli/commands/monitor.py +9 -7
- claude_mpm/cli/commands/uninstall.py +178 -0
- claude_mpm/cli/parsers/base_parser.py +8 -0
- claude_mpm/scripts/claude-hook-handler.sh +33 -7
- claude_mpm/services/cli/unified_dashboard_manager.py +14 -7
- claude_mpm/services/hook_installer_service.py +507 -0
- claude_mpm/services/monitor/daemon.py +172 -29
- claude_mpm/services/monitor/management/lifecycle.py +132 -90
- claude_mpm/services/monitor/server.py +11 -8
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/RECORD +17 -15
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.24.dist-info → claude_mpm-4.2.26.dist-info}/top_level.txt +0 -0
@@ -23,6 +23,7 @@ from pathlib import Path
|
|
23
23
|
from typing import Optional
|
24
24
|
|
25
25
|
from ...core.logging_config import get_logger
|
26
|
+
from ..hook_installer_service import HookInstallerService
|
26
27
|
from .management.health import HealthMonitor
|
27
28
|
from .management.lifecycle import DaemonLifecycle
|
28
29
|
from .server import UnifiedMonitorServer
|
@@ -59,9 +60,9 @@ class UnifiedMonitorDaemon:
|
|
59
60
|
|
60
61
|
# Daemon management with port for verification
|
61
62
|
self.lifecycle = DaemonLifecycle(
|
62
|
-
pid_file=pid_file or self._get_default_pid_file(),
|
63
|
+
pid_file=pid_file or self._get_default_pid_file(),
|
63
64
|
log_file=log_file,
|
64
|
-
port=port
|
65
|
+
port=port,
|
65
66
|
)
|
66
67
|
|
67
68
|
# Core server
|
@@ -70,6 +71,9 @@ class UnifiedMonitorDaemon:
|
|
70
71
|
# Health monitoring
|
71
72
|
self.health_monitor = HealthMonitor(port=port)
|
72
73
|
|
74
|
+
# Hook installer service
|
75
|
+
self.hook_installer = HookInstallerService()
|
76
|
+
|
73
77
|
# State
|
74
78
|
self.running = False
|
75
79
|
self.shutdown_event = threading.Event()
|
@@ -98,9 +102,90 @@ class UnifiedMonitorDaemon:
|
|
98
102
|
self.logger.error(f"Failed to start unified monitor daemon: {e}")
|
99
103
|
return False
|
100
104
|
|
105
|
+
def _cleanup_port_conflicts(self) -> bool:
|
106
|
+
"""Try to clean up any processes using our port.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
True if cleanup was successful, False otherwise
|
110
|
+
"""
|
111
|
+
try:
|
112
|
+
# Find process using the port
|
113
|
+
import subprocess
|
114
|
+
result = subprocess.run(
|
115
|
+
["lsof", "-ti", f":{self.port}"],
|
116
|
+
capture_output=True,
|
117
|
+
text=True
|
118
|
+
)
|
119
|
+
|
120
|
+
if result.returncode == 0 and result.stdout.strip():
|
121
|
+
pids = result.stdout.strip().split('\n')
|
122
|
+
for pid_str in pids:
|
123
|
+
try:
|
124
|
+
pid = int(pid_str.strip())
|
125
|
+
self.logger.info(f"Found process {pid} using port {self.port}")
|
126
|
+
|
127
|
+
# Check if it's a claude-mpm process
|
128
|
+
process_info = subprocess.run(
|
129
|
+
["ps", "-p", str(pid), "-o", "comm="],
|
130
|
+
capture_output=True,
|
131
|
+
text=True
|
132
|
+
)
|
133
|
+
|
134
|
+
if "python" in process_info.stdout.lower() or "claude" in process_info.stdout.lower():
|
135
|
+
self.logger.info(f"Killing process {pid} (appears to be Python/Claude related)")
|
136
|
+
os.kill(pid, signal.SIGTERM)
|
137
|
+
time.sleep(1)
|
138
|
+
|
139
|
+
# Check if still alive
|
140
|
+
try:
|
141
|
+
os.kill(pid, 0)
|
142
|
+
# Still alive, force kill
|
143
|
+
self.logger.warning(f"Process {pid} didn't terminate, force killing")
|
144
|
+
os.kill(pid, signal.SIGKILL)
|
145
|
+
time.sleep(1)
|
146
|
+
except ProcessLookupError:
|
147
|
+
pass
|
148
|
+
else:
|
149
|
+
self.logger.warning(f"Process {pid} is not a Claude MPM process: {process_info.stdout}")
|
150
|
+
return False
|
151
|
+
except (ValueError, ProcessLookupError) as e:
|
152
|
+
self.logger.debug(f"Error handling PID {pid_str}: {e}")
|
153
|
+
continue
|
154
|
+
|
155
|
+
return True
|
156
|
+
|
157
|
+
except FileNotFoundError:
|
158
|
+
# lsof not available, try alternative method
|
159
|
+
self.logger.debug("lsof not available, using alternative cleanup")
|
160
|
+
|
161
|
+
# Check if there's an orphaned service we can identify
|
162
|
+
is_ours, pid = self.lifecycle.is_our_service(self.host)
|
163
|
+
if is_ours and pid:
|
164
|
+
try:
|
165
|
+
self.logger.info(f"Killing orphaned Claude MPM service (PID: {pid})")
|
166
|
+
os.kill(pid, signal.SIGTERM)
|
167
|
+
time.sleep(1)
|
168
|
+
|
169
|
+
# Check if still alive
|
170
|
+
try:
|
171
|
+
os.kill(pid, 0)
|
172
|
+
os.kill(pid, signal.SIGKILL)
|
173
|
+
time.sleep(1)
|
174
|
+
except ProcessLookupError:
|
175
|
+
pass
|
176
|
+
|
177
|
+
return True
|
178
|
+
except Exception as e:
|
179
|
+
self.logger.error(f"Failed to kill process: {e}")
|
180
|
+
|
181
|
+
except Exception as e:
|
182
|
+
self.logger.error(f"Error during port cleanup: {e}")
|
183
|
+
|
184
|
+
return False
|
185
|
+
|
101
186
|
def _start_daemon(self, force_restart: bool = False) -> bool:
|
102
187
|
"""Start as background daemon process.
|
103
|
-
|
188
|
+
|
104
189
|
Args:
|
105
190
|
force_restart: If True, restart existing service if it's ours
|
106
191
|
"""
|
@@ -109,14 +194,18 @@ class UnifiedMonitorDaemon:
|
|
109
194
|
# Check if already running
|
110
195
|
if self.lifecycle.is_running():
|
111
196
|
existing_pid = self.lifecycle.get_pid()
|
112
|
-
|
197
|
+
|
113
198
|
if force_restart:
|
114
199
|
# Check if it's our service
|
115
|
-
self.logger.debug(
|
200
|
+
self.logger.debug(
|
201
|
+
f"Checking if existing daemon (PID: {existing_pid}) is our service..."
|
202
|
+
)
|
116
203
|
is_ours, detected_pid = self.lifecycle.is_our_service(self.host)
|
117
|
-
|
204
|
+
|
118
205
|
if is_ours:
|
119
|
-
self.logger.info(
|
206
|
+
self.logger.info(
|
207
|
+
f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})"
|
208
|
+
)
|
120
209
|
# Stop the existing daemon
|
121
210
|
if self.lifecycle.stop_daemon():
|
122
211
|
# Wait a moment for port to be released
|
@@ -125,19 +214,27 @@ class UnifiedMonitorDaemon:
|
|
125
214
|
self.logger.error("Failed to stop existing daemon for restart")
|
126
215
|
return False
|
127
216
|
else:
|
128
|
-
self.logger.warning(
|
129
|
-
|
217
|
+
self.logger.warning(
|
218
|
+
f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart."
|
219
|
+
)
|
220
|
+
self.logger.info(
|
221
|
+
"To restart the claude-mpm monitor, first stop the other service or use a different port."
|
222
|
+
)
|
130
223
|
return False
|
131
224
|
else:
|
132
225
|
self.logger.warning(f"Daemon already running with PID {existing_pid}")
|
133
226
|
return False
|
134
|
-
|
227
|
+
|
135
228
|
# Check for orphaned processes (service running but no PID file)
|
136
229
|
elif force_restart:
|
137
|
-
self.logger.debug(
|
230
|
+
self.logger.debug(
|
231
|
+
"No PID file found, checking for orphaned claude-mpm service..."
|
232
|
+
)
|
138
233
|
is_ours, pid = self.lifecycle.is_our_service(self.host)
|
139
234
|
if is_ours and pid:
|
140
|
-
self.logger.info(
|
235
|
+
self.logger.info(
|
236
|
+
f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
|
237
|
+
)
|
141
238
|
# Try to kill the orphaned process
|
142
239
|
try:
|
143
240
|
os.kill(pid, signal.SIGTERM)
|
@@ -155,13 +252,27 @@ class UnifiedMonitorDaemon:
|
|
155
252
|
except Exception as e:
|
156
253
|
self.logger.error(f"Failed to kill orphaned process: {e}")
|
157
254
|
return False
|
158
|
-
|
159
|
-
#
|
255
|
+
|
256
|
+
# Check port availability and clean up if needed
|
160
257
|
port_available, error_msg = self.lifecycle.verify_port_available(self.host)
|
161
258
|
if not port_available:
|
162
|
-
self.logger.
|
163
|
-
|
164
|
-
|
259
|
+
self.logger.warning(f"Port {self.port} is not available: {error_msg}")
|
260
|
+
|
261
|
+
# Try to identify and kill any process using the port
|
262
|
+
self.logger.info("Attempting to clean up processes on port...")
|
263
|
+
cleaned = self._cleanup_port_conflicts()
|
264
|
+
|
265
|
+
if cleaned:
|
266
|
+
# Wait a moment for port to be released
|
267
|
+
time.sleep(2)
|
268
|
+
# Check again
|
269
|
+
port_available, error_msg = self.lifecycle.verify_port_available(self.host)
|
270
|
+
|
271
|
+
if not port_available:
|
272
|
+
self.logger.error(f"Port {self.port} is still not available after cleanup: {error_msg}")
|
273
|
+
print(f"Error: {error_msg}", file=sys.stderr)
|
274
|
+
print(f"Try 'claude-mpm monitor stop' or use --force flag", file=sys.stderr)
|
275
|
+
return False
|
165
276
|
|
166
277
|
# Wait for any pre-warming threads to complete before forking
|
167
278
|
self._wait_for_prewarm_completion()
|
@@ -186,7 +297,7 @@ class UnifiedMonitorDaemon:
|
|
186
297
|
|
187
298
|
def _start_foreground(self, force_restart: bool = False) -> bool:
|
188
299
|
"""Start in foreground mode.
|
189
|
-
|
300
|
+
|
190
301
|
Args:
|
191
302
|
force_restart: If True, restart existing service if it's ours
|
192
303
|
"""
|
@@ -195,14 +306,18 @@ class UnifiedMonitorDaemon:
|
|
195
306
|
# Check if already running (check PID file even in foreground mode)
|
196
307
|
if self.lifecycle.is_running():
|
197
308
|
existing_pid = self.lifecycle.get_pid()
|
198
|
-
|
309
|
+
|
199
310
|
if force_restart:
|
200
311
|
# Check if it's our service
|
201
|
-
self.logger.debug(
|
312
|
+
self.logger.debug(
|
313
|
+
f"Checking if existing daemon (PID: {existing_pid}) is our service..."
|
314
|
+
)
|
202
315
|
is_ours, detected_pid = self.lifecycle.is_our_service(self.host)
|
203
|
-
|
316
|
+
|
204
317
|
if is_ours:
|
205
|
-
self.logger.info(
|
318
|
+
self.logger.info(
|
319
|
+
f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})"
|
320
|
+
)
|
206
321
|
# Stop the existing daemon
|
207
322
|
if self.lifecycle.stop_daemon():
|
208
323
|
# Wait a moment for port to be released
|
@@ -211,21 +326,29 @@ class UnifiedMonitorDaemon:
|
|
211
326
|
self.logger.error("Failed to stop existing daemon for restart")
|
212
327
|
return False
|
213
328
|
else:
|
214
|
-
self.logger.warning(
|
215
|
-
|
329
|
+
self.logger.warning(
|
330
|
+
f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart."
|
331
|
+
)
|
332
|
+
self.logger.info(
|
333
|
+
"To restart the claude-mpm monitor, first stop the other service or use a different port."
|
334
|
+
)
|
216
335
|
return False
|
217
336
|
else:
|
218
337
|
self.logger.warning(
|
219
338
|
f"Monitor daemon already running with PID {existing_pid}"
|
220
339
|
)
|
221
340
|
return False
|
222
|
-
|
341
|
+
|
223
342
|
# Check for orphaned processes (service running but no PID file)
|
224
343
|
elif force_restart:
|
225
|
-
self.logger.debug(
|
344
|
+
self.logger.debug(
|
345
|
+
"No PID file found, checking for orphaned claude-mpm service..."
|
346
|
+
)
|
226
347
|
is_ours, pid = self.lifecycle.is_our_service(self.host)
|
227
348
|
if is_ours and pid:
|
228
|
-
self.logger.info(
|
349
|
+
self.logger.info(
|
350
|
+
f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
|
351
|
+
)
|
229
352
|
# Try to kill the orphaned process
|
230
353
|
try:
|
231
354
|
os.kill(pid, signal.SIGTERM)
|
@@ -271,6 +394,26 @@ class UnifiedMonitorDaemon:
|
|
271
394
|
self.lifecycle._report_startup_error(error_msg)
|
272
395
|
return False
|
273
396
|
|
397
|
+
# Check and install hooks if needed
|
398
|
+
try:
|
399
|
+
if not self.hook_installer.is_hooks_configured():
|
400
|
+
self.logger.info("Claude Code hooks not configured, installing...")
|
401
|
+
if self.hook_installer.install_hooks():
|
402
|
+
self.logger.info("Claude Code hooks installed successfully")
|
403
|
+
else:
|
404
|
+
# Don't fail startup if hook installation fails
|
405
|
+
# The monitor can still function without hooks
|
406
|
+
self.logger.warning(
|
407
|
+
"Failed to install Claude Code hooks. Monitor will run without hook integration."
|
408
|
+
)
|
409
|
+
else:
|
410
|
+
self.logger.info("Claude Code hooks are already configured")
|
411
|
+
except Exception as e:
|
412
|
+
# Don't fail startup if hook checking fails
|
413
|
+
self.logger.warning(
|
414
|
+
f"Error checking/installing hooks: {e}. Monitor will run without hook integration."
|
415
|
+
)
|
416
|
+
|
274
417
|
# Start health monitoring
|
275
418
|
self.health_monitor.start()
|
276
419
|
|
@@ -285,7 +428,7 @@ class UnifiedMonitorDaemon:
|
|
285
428
|
|
286
429
|
self.running = True
|
287
430
|
self.logger.info("Unified monitor daemon started successfully")
|
288
|
-
|
431
|
+
|
289
432
|
# Report successful startup to parent (for daemon mode)
|
290
433
|
if self.daemon_mode:
|
291
434
|
self.lifecycle._report_startup_success()
|
@@ -326,7 +469,7 @@ class UnifiedMonitorDaemon:
|
|
326
469
|
pid = self.lifecycle.get_pid()
|
327
470
|
if pid and pid != os.getpid():
|
328
471
|
# We're not the daemon process, so stop it via signal
|
329
|
-
|
472
|
+
# Don't log here - lifecycle.stop_daemon will log
|
330
473
|
success = self.lifecycle.stop_daemon()
|
331
474
|
if success:
|
332
475
|
# Clean up our local state
|