claude-mpm 1.0.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 (38) hide show
  1. claude_mpm/_version.py +3 -2
  2. claude_mpm/agents/__init__.py +2 -2
  3. claude_mpm/agents/agent-template.yaml +83 -0
  4. claude_mpm/agents/agent_loader.py +66 -90
  5. claude_mpm/agents/base_agent_loader.py +10 -15
  6. claude_mpm/cli.py +41 -47
  7. claude_mpm/cli_enhancements.py +297 -0
  8. claude_mpm/core/factories.py +1 -46
  9. claude_mpm/core/service_registry.py +0 -8
  10. claude_mpm/core/simple_runner.py +43 -0
  11. claude_mpm/generators/__init__.py +5 -0
  12. claude_mpm/generators/agent_profile_generator.py +137 -0
  13. claude_mpm/hooks/README.md +75 -221
  14. claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
  15. claude_mpm/hooks/claude_hooks/__init__.py +5 -0
  16. claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
  17. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
  18. claude_mpm/hooks/validation_hooks.py +181 -0
  19. claude_mpm/services/agent_management_service.py +4 -4
  20. claude_mpm/services/agent_profile_loader.py +1 -1
  21. claude_mpm/services/agent_registry.py +0 -1
  22. claude_mpm/services/base_agent_manager.py +3 -3
  23. claude_mpm/utils/error_handler.py +247 -0
  24. claude_mpm/validation/__init__.py +5 -0
  25. claude_mpm/validation/agent_validator.py +175 -0
  26. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/METADATA +44 -7
  27. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/RECORD +30 -26
  28. claude_mpm/config/hook_config.py +0 -42
  29. claude_mpm/hooks/hook_client.py +0 -264
  30. claude_mpm/hooks/hook_runner.py +0 -370
  31. claude_mpm/hooks/json_rpc_executor.py +0 -259
  32. claude_mpm/hooks/json_rpc_hook_client.py +0 -319
  33. claude_mpm/services/hook_service.py +0 -388
  34. claude_mpm/services/hook_service_manager.py +0 -223
  35. claude_mpm/services/json_rpc_hook_manager.py +0 -92
  36. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/WHEEL +0 -0
  37. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/entry_points.txt +0 -0
  38. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,259 +0,0 @@
1
- """JSON-RPC based hook executor that runs hooks as subprocess calls."""
2
-
3
- import json
4
- import logging
5
- import subprocess
6
- import sys
7
- import time
8
- from pathlib import Path
9
- from typing import Any, Dict, List, Optional, Union
10
-
11
- from claude_mpm.hooks.base_hook import HookContext, HookResult, HookType
12
- from claude_mpm.core.logger import get_logger
13
-
14
- logger = get_logger(__name__)
15
-
16
-
17
- class JSONRPCError(Exception):
18
- """JSON-RPC error exception."""
19
-
20
- def __init__(self, code: int, message: str, data: Optional[Any] = None):
21
- """Initialize JSON-RPC error.
22
-
23
- Args:
24
- code: Error code
25
- message: Error message
26
- data: Optional error data
27
- """
28
- super().__init__(message)
29
- self.code = code
30
- self.message = message
31
- self.data = data
32
-
33
-
34
- class JSONRPCHookExecutor:
35
- """Executes hooks via JSON-RPC subprocess calls."""
36
-
37
- # JSON-RPC error codes
38
- PARSE_ERROR = -32700
39
- INVALID_REQUEST = -32600
40
- METHOD_NOT_FOUND = -32601
41
- INVALID_PARAMS = -32602
42
- INTERNAL_ERROR = -32603
43
-
44
- def __init__(self, timeout: int = 30):
45
- """Initialize JSON-RPC hook executor.
46
-
47
- Args:
48
- timeout: Timeout for hook execution in seconds
49
- """
50
- self.timeout = timeout
51
- self._hook_runner_path = Path(__file__).parent / "hook_runner.py"
52
-
53
- def execute_hook(self, hook_name: str, hook_type: HookType,
54
- context_data: Dict[str, Any],
55
- metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
56
- """Execute a hook via JSON-RPC subprocess.
57
-
58
- Args:
59
- hook_name: Name of the hook to execute
60
- hook_type: Type of hook
61
- context_data: Context data for the hook
62
- metadata: Optional metadata
63
-
64
- Returns:
65
- Execution result dictionary
66
-
67
- Raises:
68
- JSONRPCError: If JSON-RPC call fails
69
- """
70
- # Create JSON-RPC request
71
- request = {
72
- "jsonrpc": "2.0",
73
- "method": "execute_hook",
74
- "params": {
75
- "hook_name": hook_name,
76
- "hook_type": hook_type.value,
77
- "context_data": context_data,
78
- "metadata": metadata or {}
79
- },
80
- "id": f"{hook_name}_{int(time.time()*1000)}"
81
- }
82
-
83
- try:
84
- # Execute hook runner subprocess
85
- result = subprocess.run(
86
- [sys.executable, str(self._hook_runner_path)],
87
- input=json.dumps(request),
88
- capture_output=True,
89
- text=True,
90
- timeout=self.timeout
91
- )
92
-
93
- if result.returncode != 0:
94
- logger.error(f"Hook runner failed with code {result.returncode}: {result.stderr}")
95
- raise JSONRPCError(
96
- self.INTERNAL_ERROR,
97
- f"Hook runner process failed: {result.stderr}"
98
- )
99
-
100
- # Parse JSON-RPC response
101
- try:
102
- response = json.loads(result.stdout)
103
- except json.JSONDecodeError as e:
104
- logger.error(f"Failed to parse hook runner output: {result.stdout}")
105
- raise JSONRPCError(
106
- self.PARSE_ERROR,
107
- f"Invalid JSON response: {str(e)}"
108
- )
109
-
110
- # Check for JSON-RPC error
111
- if "error" in response:
112
- error = response["error"]
113
- raise JSONRPCError(
114
- error.get("code", self.INTERNAL_ERROR),
115
- error.get("message", "Unknown error"),
116
- error.get("data")
117
- )
118
-
119
- # Extract result
120
- if "result" not in response:
121
- raise JSONRPCError(
122
- self.INVALID_REQUEST,
123
- "Missing result in response"
124
- )
125
-
126
- return response["result"]
127
-
128
- except subprocess.TimeoutExpired:
129
- logger.error(f"Hook execution timed out after {self.timeout}s")
130
- raise JSONRPCError(
131
- self.INTERNAL_ERROR,
132
- f"Hook execution timed out after {self.timeout} seconds"
133
- )
134
- except Exception as e:
135
- if isinstance(e, JSONRPCError):
136
- raise
137
- logger.error(f"Unexpected error executing hook: {e}")
138
- raise JSONRPCError(
139
- self.INTERNAL_ERROR,
140
- f"Unexpected error: {str(e)}"
141
- )
142
-
143
- def execute_hooks(self, hook_type: HookType, hook_names: List[str],
144
- context_data: Dict[str, Any],
145
- metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
146
- """Execute multiple hooks of the same type.
147
-
148
- Args:
149
- hook_type: Type of hooks to execute
150
- hook_names: List of hook names to execute
151
- context_data: Context data for the hooks
152
- metadata: Optional metadata
153
-
154
- Returns:
155
- List of execution results
156
- """
157
- results = []
158
-
159
- for hook_name in hook_names:
160
- try:
161
- result = self.execute_hook(
162
- hook_name=hook_name,
163
- hook_type=hook_type,
164
- context_data=context_data,
165
- metadata=metadata
166
- )
167
- results.append(result)
168
- except JSONRPCError as e:
169
- # Include error in results but continue with other hooks
170
- results.append({
171
- "hook_name": hook_name,
172
- "success": False,
173
- "error": e.message,
174
- "error_code": e.code,
175
- "error_data": e.data
176
- })
177
- logger.error(f"Hook '{hook_name}' failed: {e.message}")
178
- except Exception as e:
179
- # Unexpected errors
180
- results.append({
181
- "hook_name": hook_name,
182
- "success": False,
183
- "error": str(e)
184
- })
185
- logger.error(f"Unexpected error in hook '{hook_name}': {e}")
186
-
187
- return results
188
-
189
- def batch_execute(self, requests: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
190
- """Execute multiple hook requests in batch.
191
-
192
- Args:
193
- requests: List of hook execution requests
194
-
195
- Returns:
196
- List of execution results
197
- """
198
- # Create JSON-RPC batch request
199
- batch_request = []
200
- for i, req in enumerate(requests):
201
- batch_request.append({
202
- "jsonrpc": "2.0",
203
- "method": "execute_hook",
204
- "params": req,
205
- "id": f"batch_{i}_{int(time.time()*1000)}"
206
- })
207
-
208
- try:
209
- # Execute hook runner with batch request
210
- result = subprocess.run(
211
- [sys.executable, str(self._hook_runner_path), "--batch"],
212
- input=json.dumps(batch_request),
213
- capture_output=True,
214
- text=True,
215
- timeout=self.timeout * len(requests) # Scale timeout with batch size
216
- )
217
-
218
- if result.returncode != 0:
219
- logger.error(f"Batch hook runner failed: {result.stderr}")
220
- return [{
221
- "success": False,
222
- "error": f"Batch execution failed: {result.stderr}"
223
- } for _ in requests]
224
-
225
- # Parse batch response
226
- try:
227
- responses = json.loads(result.stdout)
228
- except json.JSONDecodeError:
229
- logger.error(f"Failed to parse batch response: {result.stdout}")
230
- return [{
231
- "success": False,
232
- "error": "Invalid JSON response from batch execution"
233
- } for _ in requests]
234
-
235
- # Extract results
236
- results = []
237
- for response in responses:
238
- if "error" in response:
239
- results.append({
240
- "success": False,
241
- "error": response["error"].get("message", "Unknown error")
242
- })
243
- else:
244
- results.append(response.get("result", {"success": False}))
245
-
246
- return results
247
-
248
- except subprocess.TimeoutExpired:
249
- logger.error(f"Batch execution timed out")
250
- return [{
251
- "success": False,
252
- "error": "Batch execution timed out"
253
- } for _ in requests]
254
- except Exception as e:
255
- logger.error(f"Unexpected error in batch execution: {e}")
256
- return [{
257
- "success": False,
258
- "error": f"Unexpected error: {str(e)}"
259
- } for _ in requests]
@@ -1,319 +0,0 @@
1
- """JSON-RPC based client for executing hooks without HTTP server."""
2
-
3
- import importlib.util
4
- import inspect
5
- import logging
6
- from pathlib import Path
7
- from typing import Any, Dict, List, Optional
8
-
9
- from claude_mpm.hooks.base_hook import (
10
- BaseHook, HookType,
11
- SubmitHook, PreDelegationHook, PostDelegationHook, TicketExtractionHook
12
- )
13
- from claude_mpm.hooks.json_rpc_executor import JSONRPCHookExecutor, JSONRPCError
14
- from claude_mpm.core.logger import get_logger
15
-
16
- logger = get_logger(__name__)
17
-
18
-
19
- class JSONRPCHookClient:
20
- """Client for executing hooks via JSON-RPC subprocess calls."""
21
-
22
- def __init__(self, timeout: int = 30):
23
- """Initialize JSON-RPC hook client.
24
-
25
- Args:
26
- timeout: Request timeout in seconds
27
- """
28
- self.timeout = timeout
29
- self.executor = JSONRPCHookExecutor(timeout=timeout)
30
- self._hook_registry = {}
31
- self._hook_types = {}
32
- self._discover_hooks()
33
-
34
- def _discover_hooks(self):
35
- """Discover available hooks from builtin directory."""
36
- hooks_dir = Path(__file__).parent / 'builtin'
37
- if not hooks_dir.exists():
38
- logger.warning(f"Builtin hooks directory not found: {hooks_dir}")
39
- return
40
-
41
- for hook_file in hooks_dir.glob('*.py'):
42
- if hook_file.name.startswith('_'):
43
- continue
44
-
45
- try:
46
- # Load the module to discover hooks
47
- module_name = hook_file.stem
48
- spec = importlib.util.spec_from_file_location(module_name, hook_file)
49
- if spec and spec.loader:
50
- module = importlib.util.module_from_spec(spec)
51
- spec.loader.exec_module(module)
52
-
53
- # Find hook classes (don't instantiate, just register)
54
- for name, obj in inspect.getmembers(module):
55
- if (inspect.isclass(obj) and
56
- issubclass(obj, BaseHook) and
57
- obj not in [BaseHook, SubmitHook, PreDelegationHook,
58
- PostDelegationHook, TicketExtractionHook] and
59
- not name.startswith('_')):
60
- # Create temporary instance to get name
61
- temp_instance = obj()
62
- hook_name = temp_instance.name
63
-
64
- # Determine hook type
65
- if isinstance(temp_instance, SubmitHook):
66
- hook_type = HookType.SUBMIT
67
- elif isinstance(temp_instance, PreDelegationHook):
68
- hook_type = HookType.PRE_DELEGATION
69
- elif isinstance(temp_instance, PostDelegationHook):
70
- hook_type = HookType.POST_DELEGATION
71
- elif isinstance(temp_instance, TicketExtractionHook):
72
- hook_type = HookType.TICKET_EXTRACTION
73
- else:
74
- hook_type = HookType.CUSTOM
75
-
76
- self._hook_registry[hook_name] = {
77
- 'name': hook_name,
78
- 'type': hook_type,
79
- 'priority': temp_instance.priority,
80
- 'class': name,
81
- 'module': module_name
82
- }
83
- self._hook_types[hook_type] = self._hook_types.get(hook_type, [])
84
- self._hook_types[hook_type].append(hook_name)
85
-
86
- logger.debug(f"Discovered hook '{hook_name}' of type {hook_type.value}")
87
-
88
- except Exception as e:
89
- logger.error(f"Failed to discover hooks from {hook_file}: {e}")
90
-
91
- def health_check(self) -> Dict[str, Any]:
92
- """Check health of hook system.
93
-
94
- Returns:
95
- Health status dictionary
96
- """
97
- try:
98
- hook_count = len(self._hook_registry)
99
- return {
100
- 'status': 'healthy',
101
- 'hook_count': hook_count,
102
- 'executor': 'json-rpc',
103
- 'discovered_hooks': list(self._hook_registry.keys())
104
- }
105
- except Exception as e:
106
- logger.error(f"Health check failed: {e}")
107
- return {
108
- 'status': 'unhealthy',
109
- 'error': str(e)
110
- }
111
-
112
- def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
113
- """List all registered hooks.
114
-
115
- Returns:
116
- Dictionary mapping hook types to hook info
117
- """
118
- hooks_by_type = {}
119
-
120
- for hook_type in HookType:
121
- hooks_by_type[hook_type.value] = []
122
-
123
- for hook_name, hook_info in self._hook_registry.items():
124
- hook_type = hook_info['type']
125
- hooks_by_type[hook_type.value].append({
126
- 'name': hook_name,
127
- 'priority': hook_info['priority'],
128
- 'enabled': True # All discovered hooks are enabled
129
- })
130
-
131
- # Sort by priority
132
- for hook_list in hooks_by_type.values():
133
- hook_list.sort(key=lambda x: x['priority'])
134
-
135
- return hooks_by_type
136
-
137
- def execute_hook(self, hook_type: HookType, context_data: Dict[str, Any],
138
- metadata: Optional[Dict[str, Any]] = None,
139
- specific_hook: Optional[str] = None) -> List[Dict[str, Any]]:
140
- """Execute hooks of a given type.
141
-
142
- Args:
143
- hook_type: Type of hooks to execute
144
- context_data: Data to pass to hooks
145
- metadata: Optional metadata
146
- specific_hook: Optional specific hook name to execute
147
-
148
- Returns:
149
- List of execution results
150
- """
151
- try:
152
- if specific_hook:
153
- # Execute specific hook
154
- if specific_hook not in self._hook_registry:
155
- logger.error(f"Hook '{specific_hook}' not found")
156
- return [{
157
- 'hook_name': specific_hook,
158
- 'success': False,
159
- 'error': f"Hook '{specific_hook}' not found"
160
- }]
161
-
162
- try:
163
- result = self.executor.execute_hook(
164
- hook_name=specific_hook,
165
- hook_type=hook_type,
166
- context_data=context_data,
167
- metadata=metadata
168
- )
169
- return [result]
170
- except JSONRPCError as e:
171
- return [{
172
- 'hook_name': specific_hook,
173
- 'success': False,
174
- 'error': e.message,
175
- 'error_code': e.code
176
- }]
177
-
178
- else:
179
- # Execute all hooks of the given type
180
- hook_names = self._hook_types.get(hook_type, [])
181
- if not hook_names:
182
- logger.debug(f"No hooks registered for type {hook_type.value}")
183
- return []
184
-
185
- # Sort by priority
186
- sorted_hooks = sorted(
187
- hook_names,
188
- key=lambda name: self._hook_registry[name]['priority']
189
- )
190
-
191
- return self.executor.execute_hooks(
192
- hook_type=hook_type,
193
- hook_names=sorted_hooks,
194
- context_data=context_data,
195
- metadata=metadata
196
- )
197
-
198
- except Exception as e:
199
- logger.error(f"Failed to execute hooks: {e}")
200
- return [{
201
- 'success': False,
202
- 'error': str(e)
203
- }]
204
-
205
- def execute_submit_hook(self, prompt: str, **kwargs) -> List[Dict[str, Any]]:
206
- """Execute submit hooks on a user prompt.
207
-
208
- Args:
209
- prompt: User prompt to process
210
- **kwargs: Additional context data
211
-
212
- Returns:
213
- List of execution results
214
- """
215
- context_data = {'prompt': prompt}
216
- context_data.update(kwargs)
217
- return self.execute_hook(HookType.SUBMIT, context_data)
218
-
219
- def execute_pre_delegation_hook(self, agent: str, context: Dict[str, Any],
220
- **kwargs) -> List[Dict[str, Any]]:
221
- """Execute pre-delegation hooks.
222
-
223
- Args:
224
- agent: Agent being delegated to
225
- context: Context being passed to agent
226
- **kwargs: Additional data
227
-
228
- Returns:
229
- List of execution results
230
- """
231
- context_data = {
232
- 'agent': agent,
233
- 'context': context
234
- }
235
- context_data.update(kwargs)
236
- return self.execute_hook(HookType.PRE_DELEGATION, context_data)
237
-
238
- def execute_post_delegation_hook(self, agent: str, result: Any,
239
- **kwargs) -> List[Dict[str, Any]]:
240
- """Execute post-delegation hooks.
241
-
242
- Args:
243
- agent: Agent that was delegated to
244
- result: Result from agent
245
- **kwargs: Additional data
246
-
247
- Returns:
248
- List of execution results
249
- """
250
- context_data = {
251
- 'agent': agent,
252
- 'result': result
253
- }
254
- context_data.update(kwargs)
255
- return self.execute_hook(HookType.POST_DELEGATION, context_data)
256
-
257
- def execute_ticket_extraction_hook(self, content: Any,
258
- **kwargs) -> List[Dict[str, Any]]:
259
- """Execute ticket extraction hooks.
260
-
261
- Args:
262
- content: Content to extract tickets from
263
- **kwargs: Additional data
264
-
265
- Returns:
266
- List of execution results
267
- """
268
- context_data = {'content': content}
269
- context_data.update(kwargs)
270
- return self.execute_hook(HookType.TICKET_EXTRACTION, context_data)
271
-
272
- def get_modified_data(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
273
- """Extract modified data from hook results.
274
-
275
- Args:
276
- results: Hook execution results
277
-
278
- Returns:
279
- Combined modified data from all hooks
280
- """
281
- modified_data = {}
282
-
283
- for result in results:
284
- if result.get('modified') and result.get('data'):
285
- modified_data.update(result['data'])
286
-
287
- return modified_data
288
-
289
- def get_extracted_tickets(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
290
- """Extract tickets from hook results.
291
-
292
- Args:
293
- results: Hook execution results
294
-
295
- Returns:
296
- List of extracted tickets
297
- """
298
- all_tickets = []
299
-
300
- for result in results:
301
- if result.get('success') and 'tickets' in result.get('data', {}):
302
- tickets = result['data']['tickets']
303
- if isinstance(tickets, list):
304
- all_tickets.extend(tickets)
305
-
306
- return all_tickets
307
-
308
-
309
- # Update the convenience function to use JSON-RPC client
310
- def get_hook_client(base_url: Optional[str] = None) -> JSONRPCHookClient:
311
- """Get a hook client instance.
312
-
313
- Args:
314
- base_url: Ignored for JSON-RPC client (kept for compatibility)
315
-
316
- Returns:
317
- JSONRPCHookClient instance
318
- """
319
- return JSONRPCHookClient()