claude-mpm 0.3.0__py3-none-any.whl → 1.1.0__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.py +3 -2
- claude_mpm/agents/INSTRUCTIONS.md +23 -0
- claude_mpm/agents/__init__.py +2 -2
- claude_mpm/agents/agent-template.yaml +83 -0
- claude_mpm/agents/agent_loader.py +66 -90
- claude_mpm/agents/base_agent_loader.py +10 -15
- claude_mpm/cli.py +41 -47
- claude_mpm/cli_enhancements.py +297 -0
- claude_mpm/core/agent_name_normalizer.py +49 -0
- claude_mpm/core/factories.py +1 -46
- claude_mpm/core/service_registry.py +0 -8
- claude_mpm/core/simple_runner.py +50 -0
- claude_mpm/generators/__init__.py +5 -0
- claude_mpm/generators/agent_profile_generator.py +137 -0
- claude_mpm/hooks/README.md +75 -221
- claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +8 -7
- claude_mpm/hooks/claude_hooks/__init__.py +5 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
- claude_mpm/hooks/validation_hooks.py +181 -0
- claude_mpm/services/agent_management_service.py +4 -4
- claude_mpm/services/agent_profile_loader.py +1 -1
- claude_mpm/services/agent_registry.py +0 -1
- claude_mpm/services/base_agent_manager.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +57 -31
- claude_mpm/utils/error_handler.py +247 -0
- claude_mpm/validation/__init__.py +5 -0
- claude_mpm/validation/agent_validator.py +175 -0
- {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/METADATA +44 -7
- {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/RECORD +34 -30
- claude_mpm/config/hook_config.py +0 -42
- claude_mpm/hooks/hook_client.py +0 -264
- claude_mpm/hooks/hook_runner.py +0 -370
- claude_mpm/hooks/json_rpc_executor.py +0 -259
- claude_mpm/hooks/json_rpc_hook_client.py +0 -319
- claude_mpm/services/hook_service.py +0 -388
- claude_mpm/services/hook_service_manager.py +0 -223
- claude_mpm/services/json_rpc_hook_manager.py +0 -92
- {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""Hook service lifecycle manager for claude-mpm."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
import socket
|
|
7
|
-
import signal
|
|
8
|
-
import atexit
|
|
9
|
-
import subprocess
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Optional, Tuple
|
|
12
|
-
import json
|
|
13
|
-
import psutil
|
|
14
|
-
import logging
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from ..core.logger import get_logger
|
|
18
|
-
from ..config.hook_config import HookConfig
|
|
19
|
-
except ImportError:
|
|
20
|
-
from core.logger import get_logger
|
|
21
|
-
from config.hook_config import HookConfig
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class HookServiceManager:
|
|
25
|
-
"""Manages the hook service lifecycle."""
|
|
26
|
-
|
|
27
|
-
def __init__(self, port: Optional[int] = None, log_dir: Optional[Path] = None):
|
|
28
|
-
"""Initialize hook service manager.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
port: Specific port to use (if None, will find available port)
|
|
32
|
-
log_dir: Directory for hook service logs
|
|
33
|
-
"""
|
|
34
|
-
self.logger = get_logger("hook_service_manager")
|
|
35
|
-
self.config = HookConfig()
|
|
36
|
-
self.port = port or self._find_available_port()
|
|
37
|
-
self.log_dir = log_dir or self.config.HOOK_SERVICE_LOG_DIR
|
|
38
|
-
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
|
|
40
|
-
# Process management
|
|
41
|
-
self.pid_dir = self.config.HOOK_SERVICE_PID_DIR
|
|
42
|
-
self.pid_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
self.pid_file = self.pid_dir / f"hook_service_{self.port}.pid"
|
|
44
|
-
self.process = None
|
|
45
|
-
self._service_started = False
|
|
46
|
-
|
|
47
|
-
# Register cleanup
|
|
48
|
-
atexit.register(self.stop_service)
|
|
49
|
-
signal.signal(signal.SIGTERM, lambda sig, frame: self.stop_service())
|
|
50
|
-
signal.signal(signal.SIGINT, lambda sig, frame: self.stop_service())
|
|
51
|
-
|
|
52
|
-
def _find_available_port(self) -> int:
|
|
53
|
-
"""Find an available port in the configured range."""
|
|
54
|
-
start = self.config.PORT_RANGE_START
|
|
55
|
-
end = self.config.PORT_RANGE_END
|
|
56
|
-
for port in range(start, end):
|
|
57
|
-
if self._is_port_available(port):
|
|
58
|
-
return port
|
|
59
|
-
raise RuntimeError(f"No available ports found in range {start}-{end}")
|
|
60
|
-
|
|
61
|
-
def _is_port_available(self, port: int) -> bool:
|
|
62
|
-
"""Check if a port is available."""
|
|
63
|
-
try:
|
|
64
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
65
|
-
s.bind(('localhost', port))
|
|
66
|
-
return True
|
|
67
|
-
except OSError:
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
|
-
def _check_existing_service(self) -> bool:
|
|
71
|
-
"""Check if hook service is already running on the port."""
|
|
72
|
-
if self.pid_file.exists():
|
|
73
|
-
try:
|
|
74
|
-
pid = int(self.pid_file.read_text().strip())
|
|
75
|
-
# Check if process exists and is our hook service
|
|
76
|
-
if psutil.pid_exists(pid):
|
|
77
|
-
process = psutil.Process(pid)
|
|
78
|
-
if 'python' in process.name().lower() and 'hook_service.py' in ' '.join(process.cmdline()):
|
|
79
|
-
# Test if service is responsive
|
|
80
|
-
if self._test_service_health():
|
|
81
|
-
self.logger.info(f"Found existing hook service on port {self.port} (PID: {pid})")
|
|
82
|
-
return True
|
|
83
|
-
else:
|
|
84
|
-
self.logger.warning(f"Existing hook service on port {self.port} is not responding")
|
|
85
|
-
self._cleanup_stale_pidfile()
|
|
86
|
-
except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
87
|
-
self.logger.debug(f"Error checking existing service: {e}")
|
|
88
|
-
self._cleanup_stale_pidfile()
|
|
89
|
-
|
|
90
|
-
return False
|
|
91
|
-
|
|
92
|
-
def _cleanup_stale_pidfile(self):
|
|
93
|
-
"""Remove stale PID file."""
|
|
94
|
-
if self.pid_file.exists():
|
|
95
|
-
self.pid_file.unlink()
|
|
96
|
-
|
|
97
|
-
def _test_service_health(self) -> bool:
|
|
98
|
-
"""Test if the hook service is healthy."""
|
|
99
|
-
import requests
|
|
100
|
-
try:
|
|
101
|
-
url = self.config.get_hook_service_url(self.port) + self.config.HEALTH_ENDPOINT
|
|
102
|
-
response = requests.get(url, timeout=self.config.HEALTH_CHECK_TIMEOUT)
|
|
103
|
-
return response.status_code == 200
|
|
104
|
-
except:
|
|
105
|
-
return False
|
|
106
|
-
|
|
107
|
-
def start_service(self, force: bool = False) -> bool:
|
|
108
|
-
"""Start the hook service if not already running.
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
force: Force restart even if service is already running
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
True if service was started or already running, False on error
|
|
115
|
-
"""
|
|
116
|
-
# Check if already running
|
|
117
|
-
if not force and self._check_existing_service():
|
|
118
|
-
self._service_started = True
|
|
119
|
-
return True
|
|
120
|
-
|
|
121
|
-
# Stop any existing service if forcing
|
|
122
|
-
if force:
|
|
123
|
-
self.stop_service()
|
|
124
|
-
|
|
125
|
-
# Start new service
|
|
126
|
-
try:
|
|
127
|
-
# Find hook service script
|
|
128
|
-
hook_service_path = self._find_hook_service_script()
|
|
129
|
-
if not hook_service_path:
|
|
130
|
-
self.logger.warning("Hook service script not found")
|
|
131
|
-
return False
|
|
132
|
-
|
|
133
|
-
# Prepare log files
|
|
134
|
-
stdout_log = self.log_dir / f"hook_service_{self.port}.log"
|
|
135
|
-
stderr_log = self.log_dir / f"hook_service_{self.port}.error.log"
|
|
136
|
-
|
|
137
|
-
# Start subprocess
|
|
138
|
-
cmd = [
|
|
139
|
-
sys.executable,
|
|
140
|
-
str(hook_service_path),
|
|
141
|
-
"--port", str(self.port),
|
|
142
|
-
"--log-level", "INFO"
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
self.logger.info(f"Starting hook service on port {self.port}")
|
|
146
|
-
|
|
147
|
-
with open(stdout_log, 'a') as stdout_file, open(stderr_log, 'a') as stderr_file:
|
|
148
|
-
self.process = subprocess.Popen(
|
|
149
|
-
cmd,
|
|
150
|
-
stdout=stdout_file,
|
|
151
|
-
stderr=stderr_file,
|
|
152
|
-
start_new_session=True # Detach from parent process group
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Wait for service to start
|
|
156
|
-
start_wait = int(self.config.SERVICE_START_TIMEOUT * 10) # Convert to tenths of seconds
|
|
157
|
-
for _ in range(start_wait):
|
|
158
|
-
if self._test_service_health():
|
|
159
|
-
# Save PID
|
|
160
|
-
self.pid_file.write_text(str(self.process.pid))
|
|
161
|
-
self._service_started = True
|
|
162
|
-
self.logger.info(f"Hook service started successfully on port {self.port} (PID: {self.process.pid})")
|
|
163
|
-
return True
|
|
164
|
-
time.sleep(0.1)
|
|
165
|
-
|
|
166
|
-
# Service didn't start properly
|
|
167
|
-
self.logger.error("Hook service failed to start within timeout")
|
|
168
|
-
self.stop_service()
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
self.logger.error(f"Failed to start hook service: {e}")
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
def _find_hook_service_script(self) -> Optional[Path]:
|
|
176
|
-
"""Find the hook service script."""
|
|
177
|
-
# Try different locations
|
|
178
|
-
possible_paths = [
|
|
179
|
-
# Relative to this file (same directory)
|
|
180
|
-
Path(__file__).parent / "hook_service.py",
|
|
181
|
-
# In src/services directory
|
|
182
|
-
Path(__file__).parent.parent / "services" / "hook_service.py",
|
|
183
|
-
# In project root
|
|
184
|
-
Path(__file__).parent.parent.parent / "hook_service.py",
|
|
185
|
-
]
|
|
186
|
-
|
|
187
|
-
for path in possible_paths:
|
|
188
|
-
if path.exists():
|
|
189
|
-
return path
|
|
190
|
-
|
|
191
|
-
return None
|
|
192
|
-
|
|
193
|
-
def stop_service(self):
|
|
194
|
-
"""Stop the hook service if running."""
|
|
195
|
-
if self.process and self.process.poll() is None:
|
|
196
|
-
self.logger.info(f"Stopping hook service on port {self.port}")
|
|
197
|
-
try:
|
|
198
|
-
self.process.terminate()
|
|
199
|
-
self.process.wait(timeout=5)
|
|
200
|
-
except subprocess.TimeoutExpired:
|
|
201
|
-
self.process.kill()
|
|
202
|
-
self.process.wait()
|
|
203
|
-
finally:
|
|
204
|
-
self.process = None
|
|
205
|
-
|
|
206
|
-
# Clean up PID file
|
|
207
|
-
if self.pid_file.exists():
|
|
208
|
-
self.pid_file.unlink()
|
|
209
|
-
|
|
210
|
-
self._service_started = False
|
|
211
|
-
|
|
212
|
-
def get_service_info(self) -> dict:
|
|
213
|
-
"""Get information about the running service."""
|
|
214
|
-
return {
|
|
215
|
-
"running": self._service_started and self._test_service_health(),
|
|
216
|
-
"port": self.port,
|
|
217
|
-
"pid": self.process.pid if self.process else None,
|
|
218
|
-
"url": self.config.get_hook_service_url(self.port)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
def is_available(self) -> bool:
|
|
222
|
-
"""Check if hook service is available and healthy."""
|
|
223
|
-
return self._service_started and self._test_service_health()
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""JSON-RPC based hook manager that replaces the HTTP service."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional, Dict, Any
|
|
5
|
-
|
|
6
|
-
from ..hooks.json_rpc_hook_client import JSONRPCHookClient
|
|
7
|
-
from ..core.logger import get_logger
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class JSONRPCHookManager:
|
|
11
|
-
"""Manager for JSON-RPC based hooks (no HTTP server required)."""
|
|
12
|
-
|
|
13
|
-
def __init__(self, log_dir: Optional[Path] = None):
|
|
14
|
-
"""Initialize JSON-RPC hook manager.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
log_dir: Log directory (unused but kept for compatibility)
|
|
18
|
-
"""
|
|
19
|
-
self.logger = get_logger(self.__class__.__name__)
|
|
20
|
-
self.log_dir = log_dir
|
|
21
|
-
self.client = None
|
|
22
|
-
self._available = False
|
|
23
|
-
|
|
24
|
-
def start_service(self) -> bool:
|
|
25
|
-
"""Initialize the JSON-RPC hook client.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
True if initialization successful
|
|
29
|
-
"""
|
|
30
|
-
try:
|
|
31
|
-
self.client = JSONRPCHookClient()
|
|
32
|
-
health = self.client.health_check()
|
|
33
|
-
|
|
34
|
-
if health['status'] == 'healthy':
|
|
35
|
-
self._available = True
|
|
36
|
-
self.logger.info(f"JSON-RPC hook client initialized with {health['hook_count']} hooks")
|
|
37
|
-
return True
|
|
38
|
-
else:
|
|
39
|
-
self.logger.warning(f"JSON-RPC hook client unhealthy: {health.get('error', 'Unknown error')}")
|
|
40
|
-
return False
|
|
41
|
-
|
|
42
|
-
except Exception as e:
|
|
43
|
-
self.logger.error(f"Failed to initialize JSON-RPC hook client: {e}")
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
def stop_service(self):
|
|
47
|
-
"""Stop the hook service (no-op for JSON-RPC)."""
|
|
48
|
-
self._available = False
|
|
49
|
-
self.client = None
|
|
50
|
-
self.logger.info("JSON-RPC hook client stopped")
|
|
51
|
-
|
|
52
|
-
def is_available(self) -> bool:
|
|
53
|
-
"""Check if hook service is available.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
True if available
|
|
57
|
-
"""
|
|
58
|
-
return self._available and self.client is not None
|
|
59
|
-
|
|
60
|
-
def get_service_info(self) -> Dict[str, Any]:
|
|
61
|
-
"""Get service information.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Service info dictionary
|
|
65
|
-
"""
|
|
66
|
-
if not self.is_available():
|
|
67
|
-
return {
|
|
68
|
-
'running': False,
|
|
69
|
-
'type': 'json-rpc'
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
health = self.client.health_check()
|
|
73
|
-
return {
|
|
74
|
-
'running': True,
|
|
75
|
-
'type': 'json-rpc',
|
|
76
|
-
'hook_count': health.get('hook_count', 0),
|
|
77
|
-
'discovered_hooks': health.get('discovered_hooks', [])
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
def get_client(self) -> Optional[JSONRPCHookClient]:
|
|
81
|
-
"""Get the hook client instance.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
Hook client if available, None otherwise
|
|
85
|
-
"""
|
|
86
|
-
return self.client if self.is_available() else None
|
|
87
|
-
|
|
88
|
-
# Compatibility properties for HTTP service
|
|
89
|
-
@property
|
|
90
|
-
def port(self) -> Optional[int]:
|
|
91
|
-
"""Compatibility property - always returns None for JSON-RPC."""
|
|
92
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|