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,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()
|
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
"""Centralized hook service for claude-mpm."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
import time
|
|
9
|
-
import traceback
|
|
10
|
-
from collections import defaultdict
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List, Optional, Set, Type, Union
|
|
14
|
-
|
|
15
|
-
from flask import Flask, jsonify, request
|
|
16
|
-
from flask_cors import CORS
|
|
17
|
-
|
|
18
|
-
# Add parent directory to path for imports
|
|
19
|
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
20
|
-
|
|
21
|
-
from claude_mpm.hooks.base_hook import (
|
|
22
|
-
BaseHook, HookContext, HookResult, HookType,
|
|
23
|
-
SubmitHook, PreDelegationHook, PostDelegationHook, TicketExtractionHook
|
|
24
|
-
)
|
|
25
|
-
from claude_mpm.core.logger import get_logger
|
|
26
|
-
|
|
27
|
-
logger = get_logger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class HookRegistry:
|
|
31
|
-
"""Registry for managing hooks."""
|
|
32
|
-
|
|
33
|
-
def __init__(self):
|
|
34
|
-
"""Initialize empty hook registry."""
|
|
35
|
-
self._hooks: Dict[HookType, List[BaseHook]] = defaultdict(list)
|
|
36
|
-
self._hook_instances: Dict[str, BaseHook] = {}
|
|
37
|
-
self._lock = asyncio.Lock()
|
|
38
|
-
|
|
39
|
-
def register(self, hook: BaseHook, hook_type: Optional[HookType] = None) -> bool:
|
|
40
|
-
"""Register a hook instance.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
hook: Hook instance to register
|
|
44
|
-
hook_type: Optional hook type override
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
True if registered successfully
|
|
48
|
-
"""
|
|
49
|
-
try:
|
|
50
|
-
# Determine hook type
|
|
51
|
-
if hook_type is None:
|
|
52
|
-
if isinstance(hook, SubmitHook):
|
|
53
|
-
hook_type = HookType.SUBMIT
|
|
54
|
-
elif isinstance(hook, PreDelegationHook):
|
|
55
|
-
hook_type = HookType.PRE_DELEGATION
|
|
56
|
-
elif isinstance(hook, PostDelegationHook):
|
|
57
|
-
hook_type = HookType.POST_DELEGATION
|
|
58
|
-
elif isinstance(hook, TicketExtractionHook):
|
|
59
|
-
hook_type = HookType.TICKET_EXTRACTION
|
|
60
|
-
else:
|
|
61
|
-
hook_type = HookType.CUSTOM
|
|
62
|
-
|
|
63
|
-
# Check for duplicate names
|
|
64
|
-
if hook.name in self._hook_instances:
|
|
65
|
-
logger.warning(f"Hook '{hook.name}' already registered, replacing")
|
|
66
|
-
self.unregister(hook.name)
|
|
67
|
-
|
|
68
|
-
# Register hook
|
|
69
|
-
self._hooks[hook_type].append(hook)
|
|
70
|
-
self._hook_instances[hook.name] = hook
|
|
71
|
-
|
|
72
|
-
# Sort by priority
|
|
73
|
-
self._hooks[hook_type].sort()
|
|
74
|
-
|
|
75
|
-
logger.info(f"Registered hook '{hook.name}' for type {hook_type.value}")
|
|
76
|
-
return True
|
|
77
|
-
|
|
78
|
-
except Exception as e:
|
|
79
|
-
logger.error(f"Failed to register hook '{hook.name}': {e}")
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
def unregister(self, hook_name: str) -> bool:
|
|
83
|
-
"""Unregister a hook by name.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
hook_name: Name of hook to unregister
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
True if unregistered successfully
|
|
90
|
-
"""
|
|
91
|
-
try:
|
|
92
|
-
if hook_name not in self._hook_instances:
|
|
93
|
-
logger.warning(f"Hook '{hook_name}' not found")
|
|
94
|
-
return False
|
|
95
|
-
|
|
96
|
-
hook = self._hook_instances[hook_name]
|
|
97
|
-
|
|
98
|
-
# Remove from type list
|
|
99
|
-
for hook_list in self._hooks.values():
|
|
100
|
-
if hook in hook_list:
|
|
101
|
-
hook_list.remove(hook)
|
|
102
|
-
|
|
103
|
-
# Remove from instances
|
|
104
|
-
del self._hook_instances[hook_name]
|
|
105
|
-
|
|
106
|
-
logger.info(f"Unregistered hook '{hook_name}'")
|
|
107
|
-
return True
|
|
108
|
-
|
|
109
|
-
except Exception as e:
|
|
110
|
-
logger.error(f"Failed to unregister hook '{hook_name}': {e}")
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
def get_hooks(self, hook_type: HookType) -> List[BaseHook]:
|
|
114
|
-
"""Get all hooks for a given type.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
hook_type: Type of hooks to retrieve
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
List of hooks sorted by priority
|
|
121
|
-
"""
|
|
122
|
-
return [h for h in self._hooks[hook_type] if h.enabled]
|
|
123
|
-
|
|
124
|
-
def get_hook(self, hook_name: str) -> Optional[BaseHook]:
|
|
125
|
-
"""Get a specific hook by name.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
hook_name: Name of hook to retrieve
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
Hook instance or None if not found
|
|
132
|
-
"""
|
|
133
|
-
return self._hook_instances.get(hook_name)
|
|
134
|
-
|
|
135
|
-
def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
136
|
-
"""List all registered hooks.
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
Dictionary mapping hook types to hook info
|
|
140
|
-
"""
|
|
141
|
-
result = {}
|
|
142
|
-
for hook_type, hooks in self._hooks.items():
|
|
143
|
-
result[hook_type.value] = [
|
|
144
|
-
{
|
|
145
|
-
'name': h.name,
|
|
146
|
-
'priority': h.priority,
|
|
147
|
-
'enabled': h.enabled,
|
|
148
|
-
'class': h.__class__.__name__
|
|
149
|
-
}
|
|
150
|
-
for h in hooks
|
|
151
|
-
]
|
|
152
|
-
return result
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class HookService:
|
|
156
|
-
"""Centralized service for managing and executing hooks."""
|
|
157
|
-
|
|
158
|
-
def __init__(self, port: int = 5001):
|
|
159
|
-
"""Initialize hook service.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
port: Port to run service on
|
|
163
|
-
"""
|
|
164
|
-
self.port = port
|
|
165
|
-
self.registry = HookRegistry()
|
|
166
|
-
self.app = Flask(__name__)
|
|
167
|
-
CORS(self.app)
|
|
168
|
-
self._setup_routes()
|
|
169
|
-
self._load_builtin_hooks()
|
|
170
|
-
|
|
171
|
-
def _setup_routes(self):
|
|
172
|
-
"""Setup Flask routes for hook service."""
|
|
173
|
-
|
|
174
|
-
@self.app.route('/health', methods=['GET'])
|
|
175
|
-
def health():
|
|
176
|
-
"""Health check endpoint."""
|
|
177
|
-
return jsonify({
|
|
178
|
-
'status': 'healthy',
|
|
179
|
-
'timestamp': datetime.now().isoformat(),
|
|
180
|
-
'hooks_count': sum(len(h) for h in self.registry._hooks.values())
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
@self.app.route('/hooks/list', methods=['GET'])
|
|
184
|
-
def list_hooks():
|
|
185
|
-
"""List all registered hooks."""
|
|
186
|
-
return jsonify({
|
|
187
|
-
'status': 'success',
|
|
188
|
-
'hooks': self.registry.list_hooks()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
@self.app.route('/hooks/execute', methods=['POST'])
|
|
192
|
-
def execute_hook():
|
|
193
|
-
"""Execute a specific hook or all hooks of a type."""
|
|
194
|
-
try:
|
|
195
|
-
data = request.json
|
|
196
|
-
hook_type = HookType(data.get('hook_type'))
|
|
197
|
-
context_data = data.get('context', {})
|
|
198
|
-
metadata = data.get('metadata', {})
|
|
199
|
-
specific_hook = data.get('hook_name')
|
|
200
|
-
|
|
201
|
-
# Create context
|
|
202
|
-
context = HookContext(
|
|
203
|
-
hook_type=hook_type,
|
|
204
|
-
data=context_data,
|
|
205
|
-
metadata=metadata,
|
|
206
|
-
timestamp=datetime.now()
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
# Execute hooks
|
|
210
|
-
if specific_hook:
|
|
211
|
-
hook = self.registry.get_hook(specific_hook)
|
|
212
|
-
if not hook:
|
|
213
|
-
return jsonify({
|
|
214
|
-
'status': 'error',
|
|
215
|
-
'error': f"Hook '{specific_hook}' not found"
|
|
216
|
-
}), 404
|
|
217
|
-
results = [self._execute_single_hook(hook, context)]
|
|
218
|
-
else:
|
|
219
|
-
results = self._execute_hooks(hook_type, context)
|
|
220
|
-
|
|
221
|
-
return jsonify({
|
|
222
|
-
'status': 'success',
|
|
223
|
-
'results': results
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
except Exception as e:
|
|
227
|
-
logger.error(f"Hook execution error: {e}")
|
|
228
|
-
return jsonify({
|
|
229
|
-
'status': 'error',
|
|
230
|
-
'error': str(e)
|
|
231
|
-
}), 500
|
|
232
|
-
|
|
233
|
-
@self.app.route('/hooks/register', methods=['POST'])
|
|
234
|
-
def register_hook():
|
|
235
|
-
"""Register a new hook (for dynamic registration)."""
|
|
236
|
-
try:
|
|
237
|
-
data = request.json
|
|
238
|
-
# This would need to dynamically create hook instances
|
|
239
|
-
# For now, return not implemented
|
|
240
|
-
return jsonify({
|
|
241
|
-
'status': 'error',
|
|
242
|
-
'error': 'Dynamic registration not yet implemented'
|
|
243
|
-
}), 501
|
|
244
|
-
|
|
245
|
-
except Exception as e:
|
|
246
|
-
logger.error(f"Hook registration error: {e}")
|
|
247
|
-
return jsonify({
|
|
248
|
-
'status': 'error',
|
|
249
|
-
'error': str(e)
|
|
250
|
-
}), 500
|
|
251
|
-
|
|
252
|
-
def _load_builtin_hooks(self):
|
|
253
|
-
"""Load built-in hooks from hooks directory."""
|
|
254
|
-
import importlib.util
|
|
255
|
-
import inspect
|
|
256
|
-
|
|
257
|
-
hooks_dir = Path(__file__).parent.parent / 'hooks' / 'builtin'
|
|
258
|
-
if hooks_dir.exists():
|
|
259
|
-
for hook_file in hooks_dir.glob('*.py'):
|
|
260
|
-
if hook_file.name.startswith('_'):
|
|
261
|
-
continue
|
|
262
|
-
try:
|
|
263
|
-
# Load the module
|
|
264
|
-
module_name = hook_file.stem
|
|
265
|
-
spec = importlib.util.spec_from_file_location(module_name, hook_file)
|
|
266
|
-
module = importlib.util.module_from_spec(spec)
|
|
267
|
-
spec.loader.exec_module(module)
|
|
268
|
-
|
|
269
|
-
# Find and instantiate hook classes
|
|
270
|
-
for name, obj in inspect.getmembers(module):
|
|
271
|
-
if (inspect.isclass(obj) and
|
|
272
|
-
issubclass(obj, BaseHook) and
|
|
273
|
-
obj is not BaseHook and
|
|
274
|
-
not name.startswith('_')):
|
|
275
|
-
# Instantiate and register the hook
|
|
276
|
-
hook_instance = obj()
|
|
277
|
-
self.registry.register(hook_instance)
|
|
278
|
-
logger.info(f"Loaded hook '{hook_instance.name}' from {hook_file}")
|
|
279
|
-
|
|
280
|
-
except Exception as e:
|
|
281
|
-
logger.error(f"Failed to load hook from {hook_file}: {e}")
|
|
282
|
-
|
|
283
|
-
def _execute_single_hook(self, hook: BaseHook, context: HookContext) -> Dict[str, Any]:
|
|
284
|
-
"""Execute a single hook.
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
hook: Hook to execute
|
|
288
|
-
context: Context for execution
|
|
289
|
-
|
|
290
|
-
Returns:
|
|
291
|
-
Execution result dictionary
|
|
292
|
-
"""
|
|
293
|
-
start_time = time.time()
|
|
294
|
-
try:
|
|
295
|
-
# Validate hook
|
|
296
|
-
if not hook.validate(context):
|
|
297
|
-
return {
|
|
298
|
-
'hook_name': hook.name,
|
|
299
|
-
'success': False,
|
|
300
|
-
'error': 'Validation failed',
|
|
301
|
-
'execution_time_ms': 0
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
# Execute hook
|
|
305
|
-
if hasattr(hook, '_async') and hook._async:
|
|
306
|
-
# Run async hook
|
|
307
|
-
loop = asyncio.new_event_loop()
|
|
308
|
-
asyncio.set_event_loop(loop)
|
|
309
|
-
result = loop.run_until_complete(hook.async_execute(context))
|
|
310
|
-
loop.close()
|
|
311
|
-
else:
|
|
312
|
-
result = hook.execute(context)
|
|
313
|
-
|
|
314
|
-
# Add execution time
|
|
315
|
-
execution_time = (time.time() - start_time) * 1000
|
|
316
|
-
result.execution_time_ms = execution_time
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
'hook_name': hook.name,
|
|
320
|
-
'success': result.success,
|
|
321
|
-
'data': result.data,
|
|
322
|
-
'error': result.error,
|
|
323
|
-
'modified': result.modified,
|
|
324
|
-
'metadata': result.metadata,
|
|
325
|
-
'execution_time_ms': execution_time
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
except Exception as e:
|
|
329
|
-
logger.error(f"Hook '{hook.name}' execution failed: {e}")
|
|
330
|
-
return {
|
|
331
|
-
'hook_name': hook.name,
|
|
332
|
-
'success': False,
|
|
333
|
-
'error': str(e),
|
|
334
|
-
'traceback': traceback.format_exc(),
|
|
335
|
-
'execution_time_ms': (time.time() - start_time) * 1000
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
def _execute_hooks(self, hook_type: HookType, context: HookContext) -> List[Dict[str, Any]]:
|
|
339
|
-
"""Execute all hooks of a given type.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
hook_type: Type of hooks to execute
|
|
343
|
-
context: Context for execution
|
|
344
|
-
|
|
345
|
-
Returns:
|
|
346
|
-
List of execution results
|
|
347
|
-
"""
|
|
348
|
-
hooks = self.registry.get_hooks(hook_type)
|
|
349
|
-
results = []
|
|
350
|
-
|
|
351
|
-
for hook in hooks:
|
|
352
|
-
result = self._execute_single_hook(hook, context)
|
|
353
|
-
results.append(result)
|
|
354
|
-
|
|
355
|
-
# If hook modified data, update context for next hook
|
|
356
|
-
if result.get('modified') and result.get('data'):
|
|
357
|
-
context.data.update(result['data'])
|
|
358
|
-
|
|
359
|
-
return results
|
|
360
|
-
|
|
361
|
-
def run(self):
|
|
362
|
-
"""Run the hook service."""
|
|
363
|
-
logger.info(f"Starting hook service on port {self.port}")
|
|
364
|
-
self.app.run(host='0.0.0.0', port=self.port, debug=False)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def main():
|
|
368
|
-
"""Main entry point for hook service."""
|
|
369
|
-
import argparse
|
|
370
|
-
|
|
371
|
-
parser = argparse.ArgumentParser(description='Claude MPM Hook Service')
|
|
372
|
-
parser.add_argument('--port', type=int, default=5001, help='Port to run service on')
|
|
373
|
-
parser.add_argument('--log-level', default='INFO', help='Logging level')
|
|
374
|
-
args = parser.parse_args()
|
|
375
|
-
|
|
376
|
-
# Configure logging
|
|
377
|
-
logging.basicConfig(
|
|
378
|
-
level=getattr(logging, args.log_level.upper()),
|
|
379
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
# Create and run service
|
|
383
|
-
service = HookService(port=args.port)
|
|
384
|
-
service.run()
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if __name__ == '__main__':
|
|
388
|
-
main()
|