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.
Files changed (42) hide show
  1. claude_mpm/_version.py +3 -2
  2. claude_mpm/agents/INSTRUCTIONS.md +23 -0
  3. claude_mpm/agents/__init__.py +2 -2
  4. claude_mpm/agents/agent-template.yaml +83 -0
  5. claude_mpm/agents/agent_loader.py +66 -90
  6. claude_mpm/agents/base_agent_loader.py +10 -15
  7. claude_mpm/cli.py +41 -47
  8. claude_mpm/cli_enhancements.py +297 -0
  9. claude_mpm/core/agent_name_normalizer.py +49 -0
  10. claude_mpm/core/factories.py +1 -46
  11. claude_mpm/core/service_registry.py +0 -8
  12. claude_mpm/core/simple_runner.py +50 -0
  13. claude_mpm/generators/__init__.py +5 -0
  14. claude_mpm/generators/agent_profile_generator.py +137 -0
  15. claude_mpm/hooks/README.md +75 -221
  16. claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
  17. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +8 -7
  18. claude_mpm/hooks/claude_hooks/__init__.py +5 -0
  19. claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
  20. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
  21. claude_mpm/hooks/validation_hooks.py +181 -0
  22. claude_mpm/services/agent_management_service.py +4 -4
  23. claude_mpm/services/agent_profile_loader.py +1 -1
  24. claude_mpm/services/agent_registry.py +0 -1
  25. claude_mpm/services/base_agent_manager.py +3 -3
  26. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +57 -31
  27. claude_mpm/utils/error_handler.py +247 -0
  28. claude_mpm/validation/__init__.py +5 -0
  29. claude_mpm/validation/agent_validator.py +175 -0
  30. {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/METADATA +44 -7
  31. {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/RECORD +34 -30
  32. claude_mpm/config/hook_config.py +0 -42
  33. claude_mpm/hooks/hook_client.py +0 -264
  34. claude_mpm/hooks/hook_runner.py +0 -370
  35. claude_mpm/hooks/json_rpc_executor.py +0 -259
  36. claude_mpm/hooks/json_rpc_hook_client.py +0 -319
  37. claude_mpm/services/hook_service.py +0 -388
  38. claude_mpm/services/hook_service_manager.py +0 -223
  39. claude_mpm/services/json_rpc_hook_manager.py +0 -92
  40. {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/WHEEL +0 -0
  41. {claude_mpm-0.3.0.dist-info → claude_mpm-1.1.0.dist-info}/entry_points.txt +0 -0
  42. {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