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.
- claude_mpm/_version.py +3 -2
- 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/factories.py +1 -46
- claude_mpm/core/service_registry.py +0 -8
- claude_mpm/core/simple_runner.py +43 -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/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/utils/error_handler.py +247 -0
- claude_mpm/validation/__init__.py +5 -0
- claude_mpm/validation/agent_validator.py +175 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/METADATA +44 -7
- {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/RECORD +30 -26
- 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-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/top_level.txt +0 -0
claude_mpm/config/hook_config.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Configuration for hook service integration."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class HookConfig:
|
|
8
|
-
"""Hook service configuration."""
|
|
9
|
-
|
|
10
|
-
# Service settings
|
|
11
|
-
DEFAULT_PORT = 8080
|
|
12
|
-
PORT_RANGE_START = 8080
|
|
13
|
-
PORT_RANGE_END = 8090
|
|
14
|
-
|
|
15
|
-
# Timeouts
|
|
16
|
-
SERVICE_START_TIMEOUT = 3.0 # seconds
|
|
17
|
-
REQUEST_TIMEOUT = 30 # seconds
|
|
18
|
-
HEALTH_CHECK_TIMEOUT = 2.0 # seconds
|
|
19
|
-
|
|
20
|
-
# Paths
|
|
21
|
-
HOOK_SERVICE_LOG_DIR = Path.home() / ".claude-mpm" / "logs"
|
|
22
|
-
HOOK_SERVICE_PID_DIR = Path.home() / ".claude-mpm" / "run"
|
|
23
|
-
|
|
24
|
-
# Enable/disable hooks by default
|
|
25
|
-
HOOKS_ENABLED_BY_DEFAULT = True
|
|
26
|
-
|
|
27
|
-
# Hook service endpoints
|
|
28
|
-
HEALTH_ENDPOINT = "/health"
|
|
29
|
-
SUBMIT_HOOK_ENDPOINT = "/hooks/submit"
|
|
30
|
-
PRE_DELEGATION_HOOK_ENDPOINT = "/hooks/pre-delegation"
|
|
31
|
-
POST_DELEGATION_HOOK_ENDPOINT = "/hooks/post-delegation"
|
|
32
|
-
TICKET_EXTRACTION_HOOK_ENDPOINT = "/hooks/ticket-extraction"
|
|
33
|
-
|
|
34
|
-
@classmethod
|
|
35
|
-
def get_hook_service_url(cls, port: int) -> str:
|
|
36
|
-
"""Get the hook service URL for a given port."""
|
|
37
|
-
return f"http://localhost:{port}"
|
|
38
|
-
|
|
39
|
-
@classmethod
|
|
40
|
-
def is_hooks_enabled(cls) -> bool:
|
|
41
|
-
"""Check if hooks are enabled via environment variable."""
|
|
42
|
-
return os.environ.get("CLAUDE_MPM_HOOKS_ENABLED", str(cls.HOOKS_ENABLED_BY_DEFAULT)).lower() in ("true", "1", "yes")
|
claude_mpm/hooks/hook_client.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
"""Client for interacting with the hook service.
|
|
2
|
-
|
|
3
|
-
DEPRECATED: This HTTP-based hook client is deprecated and will be removed in a future release.
|
|
4
|
-
Please use the JSON-RPC implementation from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
5
|
-
See /docs/hook_system_migration_guide.md for migration instructions.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
import warnings
|
|
11
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
-
from urllib.parse import urljoin
|
|
13
|
-
|
|
14
|
-
import requests
|
|
15
|
-
from requests.adapters import HTTPAdapter
|
|
16
|
-
from requests.packages.urllib3.util.retry import Retry
|
|
17
|
-
|
|
18
|
-
from claude_mpm.hooks.base_hook import HookType
|
|
19
|
-
from claude_mpm.core.logger import get_logger
|
|
20
|
-
|
|
21
|
-
logger = get_logger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class HookServiceClient:
|
|
25
|
-
"""Client for interacting with the centralized hook service.
|
|
26
|
-
|
|
27
|
-
DEPRECATED: Use JSONRPCHookClient from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, base_url: str = "http://localhost:5001", timeout: int = 30):
|
|
31
|
-
"""Initialize hook service client.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
base_url: Base URL of hook service
|
|
35
|
-
timeout: Request timeout in seconds
|
|
36
|
-
"""
|
|
37
|
-
warnings.warn(
|
|
38
|
-
"HookServiceClient is deprecated and will be removed in a future release. "
|
|
39
|
-
"Please use JSONRPCHookClient from claude_mpm.hooks.json_rpc_hook_client instead. "
|
|
40
|
-
"See /docs/hook_system_migration_guide.md for migration instructions.",
|
|
41
|
-
DeprecationWarning,
|
|
42
|
-
stacklevel=2
|
|
43
|
-
)
|
|
44
|
-
self.base_url = base_url.rstrip('/')
|
|
45
|
-
self.timeout = timeout
|
|
46
|
-
|
|
47
|
-
# Setup session with retry logic
|
|
48
|
-
self.session = requests.Session()
|
|
49
|
-
retry_strategy = Retry(
|
|
50
|
-
total=3,
|
|
51
|
-
backoff_factor=1,
|
|
52
|
-
status_forcelist=[429, 500, 502, 503, 504]
|
|
53
|
-
)
|
|
54
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
55
|
-
self.session.mount("http://", adapter)
|
|
56
|
-
self.session.mount("https://", adapter)
|
|
57
|
-
|
|
58
|
-
def health_check(self) -> Dict[str, Any]:
|
|
59
|
-
"""Check health of hook service.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Health status dictionary
|
|
63
|
-
"""
|
|
64
|
-
try:
|
|
65
|
-
response = self.session.get(
|
|
66
|
-
urljoin(self.base_url, '/health'),
|
|
67
|
-
timeout=self.timeout
|
|
68
|
-
)
|
|
69
|
-
response.raise_for_status()
|
|
70
|
-
return response.json()
|
|
71
|
-
except Exception as e:
|
|
72
|
-
logger.error(f"Health check failed: {e}")
|
|
73
|
-
return {
|
|
74
|
-
'status': 'unhealthy',
|
|
75
|
-
'error': str(e)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
79
|
-
"""List all registered hooks.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
Dictionary mapping hook types to hook info
|
|
83
|
-
"""
|
|
84
|
-
try:
|
|
85
|
-
response = self.session.get(
|
|
86
|
-
urljoin(self.base_url, '/hooks/list'),
|
|
87
|
-
timeout=self.timeout
|
|
88
|
-
)
|
|
89
|
-
response.raise_for_status()
|
|
90
|
-
data = response.json()
|
|
91
|
-
return data.get('hooks', {})
|
|
92
|
-
except Exception as e:
|
|
93
|
-
logger.error(f"Failed to list hooks: {e}")
|
|
94
|
-
return {}
|
|
95
|
-
|
|
96
|
-
def execute_hook(self, hook_type: HookType, context_data: Dict[str, Any],
|
|
97
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
98
|
-
specific_hook: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
99
|
-
"""Execute hooks of a given type.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
hook_type: Type of hooks to execute
|
|
103
|
-
context_data: Data to pass to hooks
|
|
104
|
-
metadata: Optional metadata
|
|
105
|
-
specific_hook: Optional specific hook name to execute
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
List of execution results
|
|
109
|
-
"""
|
|
110
|
-
try:
|
|
111
|
-
payload = {
|
|
112
|
-
'hook_type': hook_type.value,
|
|
113
|
-
'context': context_data,
|
|
114
|
-
'metadata': metadata or {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if specific_hook:
|
|
118
|
-
payload['hook_name'] = specific_hook
|
|
119
|
-
|
|
120
|
-
response = self.session.post(
|
|
121
|
-
urljoin(self.base_url, '/hooks/execute'),
|
|
122
|
-
json=payload,
|
|
123
|
-
timeout=self.timeout
|
|
124
|
-
)
|
|
125
|
-
response.raise_for_status()
|
|
126
|
-
data = response.json()
|
|
127
|
-
|
|
128
|
-
if data.get('status') == 'success':
|
|
129
|
-
return data.get('results', [])
|
|
130
|
-
else:
|
|
131
|
-
logger.error(f"Hook execution failed: {data.get('error')}")
|
|
132
|
-
return []
|
|
133
|
-
|
|
134
|
-
except Exception as e:
|
|
135
|
-
logger.error(f"Failed to execute hooks: {e}")
|
|
136
|
-
return []
|
|
137
|
-
|
|
138
|
-
def execute_submit_hook(self, prompt: str, **kwargs) -> List[Dict[str, Any]]:
|
|
139
|
-
"""Execute submit hooks on a user prompt.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
prompt: User prompt to process
|
|
143
|
-
**kwargs: Additional context data
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
List of execution results
|
|
147
|
-
"""
|
|
148
|
-
context_data = {'prompt': prompt}
|
|
149
|
-
context_data.update(kwargs)
|
|
150
|
-
return self.execute_hook(HookType.SUBMIT, context_data)
|
|
151
|
-
|
|
152
|
-
def execute_pre_delegation_hook(self, agent: str, context: Dict[str, Any],
|
|
153
|
-
**kwargs) -> List[Dict[str, Any]]:
|
|
154
|
-
"""Execute pre-delegation hooks.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
agent: Agent being delegated to
|
|
158
|
-
context: Context being passed to agent
|
|
159
|
-
**kwargs: Additional data
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
List of execution results
|
|
163
|
-
"""
|
|
164
|
-
context_data = {
|
|
165
|
-
'agent': agent,
|
|
166
|
-
'context': context
|
|
167
|
-
}
|
|
168
|
-
context_data.update(kwargs)
|
|
169
|
-
return self.execute_hook(HookType.PRE_DELEGATION, context_data)
|
|
170
|
-
|
|
171
|
-
def execute_post_delegation_hook(self, agent: str, result: Any,
|
|
172
|
-
**kwargs) -> List[Dict[str, Any]]:
|
|
173
|
-
"""Execute post-delegation hooks.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
agent: Agent that was delegated to
|
|
177
|
-
result: Result from agent
|
|
178
|
-
**kwargs: Additional data
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
List of execution results
|
|
182
|
-
"""
|
|
183
|
-
context_data = {
|
|
184
|
-
'agent': agent,
|
|
185
|
-
'result': result
|
|
186
|
-
}
|
|
187
|
-
context_data.update(kwargs)
|
|
188
|
-
return self.execute_hook(HookType.POST_DELEGATION, context_data)
|
|
189
|
-
|
|
190
|
-
def execute_ticket_extraction_hook(self, content: Any,
|
|
191
|
-
**kwargs) -> List[Dict[str, Any]]:
|
|
192
|
-
"""Execute ticket extraction hooks.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
content: Content to extract tickets from
|
|
196
|
-
**kwargs: Additional data
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
List of execution results
|
|
200
|
-
"""
|
|
201
|
-
context_data = {'content': content}
|
|
202
|
-
context_data.update(kwargs)
|
|
203
|
-
return self.execute_hook(HookType.TICKET_EXTRACTION, context_data)
|
|
204
|
-
|
|
205
|
-
def get_modified_data(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
206
|
-
"""Extract modified data from hook results.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
results: Hook execution results
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Combined modified data from all hooks
|
|
213
|
-
"""
|
|
214
|
-
modified_data = {}
|
|
215
|
-
|
|
216
|
-
for result in results:
|
|
217
|
-
if result.get('modified') and result.get('data'):
|
|
218
|
-
modified_data.update(result['data'])
|
|
219
|
-
|
|
220
|
-
return modified_data
|
|
221
|
-
|
|
222
|
-
def get_extracted_tickets(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
223
|
-
"""Extract tickets from hook results.
|
|
224
|
-
|
|
225
|
-
Args:
|
|
226
|
-
results: Hook execution results
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
List of extracted tickets
|
|
230
|
-
"""
|
|
231
|
-
all_tickets = []
|
|
232
|
-
|
|
233
|
-
for result in results:
|
|
234
|
-
if result.get('success') and 'tickets' in result.get('data', {}):
|
|
235
|
-
tickets = result['data']['tickets']
|
|
236
|
-
if isinstance(tickets, list):
|
|
237
|
-
all_tickets.extend(tickets)
|
|
238
|
-
|
|
239
|
-
return all_tickets
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# Convenience function for creating a default client
|
|
243
|
-
def get_hook_client(base_url: Optional[str] = None) -> 'JSONRPCHookClient':
|
|
244
|
-
"""Get a hook client instance.
|
|
245
|
-
|
|
246
|
-
DEPRECATED: This function now returns a JSONRPCHookClient for compatibility.
|
|
247
|
-
Import directly from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
base_url: Ignored (kept for backward compatibility)
|
|
251
|
-
|
|
252
|
-
Returns:
|
|
253
|
-
JSONRPCHookClient instance
|
|
254
|
-
"""
|
|
255
|
-
warnings.warn(
|
|
256
|
-
"get_hook_client from hook_client module is deprecated. "
|
|
257
|
-
"Import from claude_mpm.hooks.json_rpc_hook_client instead.",
|
|
258
|
-
DeprecationWarning,
|
|
259
|
-
stacklevel=2
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
# Import and return JSON-RPC client for compatibility
|
|
263
|
-
from claude_mpm.hooks.json_rpc_hook_client import JSONRPCHookClient
|
|
264
|
-
return JSONRPCHookClient()
|
claude_mpm/hooks/hook_runner.py
DELETED
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
"""Hook runner subprocess entry point for JSON-RPC execution."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import inspect
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import sys
|
|
8
|
-
import traceback
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Type
|
|
12
|
-
|
|
13
|
-
# Add parent directory to path for imports
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
15
|
-
|
|
16
|
-
from claude_mpm.hooks.base_hook import (
|
|
17
|
-
BaseHook, HookContext, HookResult, HookType,
|
|
18
|
-
SubmitHook, PreDelegationHook, PostDelegationHook, TicketExtractionHook
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
# Configure logging to stderr so it doesn't interfere with stdout JSON-RPC
|
|
22
|
-
logging.basicConfig(
|
|
23
|
-
level=logging.INFO,
|
|
24
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
25
|
-
stream=sys.stderr
|
|
26
|
-
)
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class HookLoader:
|
|
31
|
-
"""Loads and manages hook instances."""
|
|
32
|
-
|
|
33
|
-
def __init__(self):
|
|
34
|
-
"""Initialize hook loader."""
|
|
35
|
-
self._hooks: Dict[str, BaseHook] = {}
|
|
36
|
-
self._hook_types: Dict[str, HookType] = {}
|
|
37
|
-
self._load_builtin_hooks()
|
|
38
|
-
|
|
39
|
-
def _load_builtin_hooks(self):
|
|
40
|
-
"""Load built-in hooks from hooks/builtin directory."""
|
|
41
|
-
hooks_dir = Path(__file__).parent / 'builtin'
|
|
42
|
-
if not hooks_dir.exists():
|
|
43
|
-
logger.warning(f"Builtin hooks directory not found: {hooks_dir}")
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
for hook_file in hooks_dir.glob('*.py'):
|
|
47
|
-
if hook_file.name.startswith('_'):
|
|
48
|
-
continue
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
# Load the module
|
|
52
|
-
module_name = hook_file.stem
|
|
53
|
-
spec = importlib.util.spec_from_file_location(module_name, hook_file)
|
|
54
|
-
if spec and spec.loader:
|
|
55
|
-
module = importlib.util.module_from_spec(spec)
|
|
56
|
-
spec.loader.exec_module(module)
|
|
57
|
-
|
|
58
|
-
# Find and instantiate hook classes
|
|
59
|
-
for name, obj in inspect.getmembers(module):
|
|
60
|
-
if (inspect.isclass(obj) and
|
|
61
|
-
issubclass(obj, BaseHook) and
|
|
62
|
-
obj not in [BaseHook, SubmitHook, PreDelegationHook,
|
|
63
|
-
PostDelegationHook, TicketExtractionHook] and
|
|
64
|
-
not name.startswith('_')):
|
|
65
|
-
# Instantiate the hook
|
|
66
|
-
hook_instance = obj()
|
|
67
|
-
self._hooks[hook_instance.name] = hook_instance
|
|
68
|
-
|
|
69
|
-
# Determine hook type
|
|
70
|
-
if isinstance(hook_instance, SubmitHook):
|
|
71
|
-
self._hook_types[hook_instance.name] = HookType.SUBMIT
|
|
72
|
-
elif isinstance(hook_instance, PreDelegationHook):
|
|
73
|
-
self._hook_types[hook_instance.name] = HookType.PRE_DELEGATION
|
|
74
|
-
elif isinstance(hook_instance, PostDelegationHook):
|
|
75
|
-
self._hook_types[hook_instance.name] = HookType.POST_DELEGATION
|
|
76
|
-
elif isinstance(hook_instance, TicketExtractionHook):
|
|
77
|
-
self._hook_types[hook_instance.name] = HookType.TICKET_EXTRACTION
|
|
78
|
-
else:
|
|
79
|
-
self._hook_types[hook_instance.name] = HookType.CUSTOM
|
|
80
|
-
|
|
81
|
-
logger.info(f"Loaded hook '{hook_instance.name}' from {hook_file}")
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
logger.error(f"Failed to load hooks from {hook_file}: {e}")
|
|
85
|
-
logger.error(traceback.format_exc())
|
|
86
|
-
|
|
87
|
-
def get_hook(self, hook_name: str) -> Optional[BaseHook]:
|
|
88
|
-
"""Get a hook by name.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
hook_name: Name of the hook
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Hook instance or None if not found
|
|
95
|
-
"""
|
|
96
|
-
return self._hooks.get(hook_name)
|
|
97
|
-
|
|
98
|
-
def get_hook_type(self, hook_name: str) -> Optional[HookType]:
|
|
99
|
-
"""Get the type of a hook by name.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
hook_name: Name of the hook
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
Hook type or None if not found
|
|
106
|
-
"""
|
|
107
|
-
return self._hook_types.get(hook_name)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class JSONRPCHookRunner:
|
|
111
|
-
"""Runs hooks in response to JSON-RPC requests."""
|
|
112
|
-
|
|
113
|
-
def __init__(self):
|
|
114
|
-
"""Initialize hook runner."""
|
|
115
|
-
self.loader = HookLoader()
|
|
116
|
-
|
|
117
|
-
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
118
|
-
"""Handle a single JSON-RPC request.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
request: JSON-RPC request object
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
JSON-RPC response object
|
|
125
|
-
"""
|
|
126
|
-
# Validate request
|
|
127
|
-
if not isinstance(request, dict):
|
|
128
|
-
return self._error_response(
|
|
129
|
-
-32600, "Invalid Request", None,
|
|
130
|
-
"Request must be an object"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
if request.get("jsonrpc") != "2.0":
|
|
134
|
-
return self._error_response(
|
|
135
|
-
-32600, "Invalid Request",
|
|
136
|
-
request.get("id"),
|
|
137
|
-
"Must specify jsonrpc: '2.0'"
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
request_id = request.get("id")
|
|
141
|
-
method = request.get("method")
|
|
142
|
-
params = request.get("params", {})
|
|
143
|
-
|
|
144
|
-
if not method:
|
|
145
|
-
return self._error_response(
|
|
146
|
-
-32600, "Invalid Request", request_id,
|
|
147
|
-
"Missing method"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Route to method handler
|
|
151
|
-
if method == "execute_hook":
|
|
152
|
-
return self._execute_hook(params, request_id)
|
|
153
|
-
else:
|
|
154
|
-
return self._error_response(
|
|
155
|
-
-32601, "Method not found", request_id,
|
|
156
|
-
f"Unknown method: {method}"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
def _execute_hook(self, params: Dict[str, Any], request_id: Any) -> Dict[str, Any]:
|
|
160
|
-
"""Execute a hook based on parameters.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
params: Hook execution parameters
|
|
164
|
-
request_id: JSON-RPC request ID
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
JSON-RPC response
|
|
168
|
-
"""
|
|
169
|
-
try:
|
|
170
|
-
# Extract parameters
|
|
171
|
-
hook_name = params.get("hook_name")
|
|
172
|
-
hook_type_str = params.get("hook_type")
|
|
173
|
-
context_data = params.get("context_data", {})
|
|
174
|
-
metadata = params.get("metadata", {})
|
|
175
|
-
|
|
176
|
-
if not hook_name:
|
|
177
|
-
return self._error_response(
|
|
178
|
-
-32602, "Invalid params", request_id,
|
|
179
|
-
"Missing hook_name parameter"
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Get hook instance
|
|
183
|
-
hook = self.loader.get_hook(hook_name)
|
|
184
|
-
if not hook:
|
|
185
|
-
return self._error_response(
|
|
186
|
-
-32602, "Invalid params", request_id,
|
|
187
|
-
f"Hook '{hook_name}' not found"
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
# Create hook context
|
|
191
|
-
try:
|
|
192
|
-
hook_type = HookType(hook_type_str) if hook_type_str else self.loader.get_hook_type(hook_name)
|
|
193
|
-
except ValueError:
|
|
194
|
-
return self._error_response(
|
|
195
|
-
-32602, "Invalid params", request_id,
|
|
196
|
-
f"Invalid hook type: {hook_type_str}"
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
context = HookContext(
|
|
200
|
-
hook_type=hook_type,
|
|
201
|
-
data=context_data,
|
|
202
|
-
metadata=metadata,
|
|
203
|
-
timestamp=datetime.now()
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# Validate hook can run
|
|
207
|
-
if not hook.validate(context):
|
|
208
|
-
return self._success_response({
|
|
209
|
-
"hook_name": hook_name,
|
|
210
|
-
"success": False,
|
|
211
|
-
"error": "Hook validation failed",
|
|
212
|
-
"skipped": True
|
|
213
|
-
}, request_id)
|
|
214
|
-
|
|
215
|
-
# Execute hook
|
|
216
|
-
import time
|
|
217
|
-
start_time = time.time()
|
|
218
|
-
|
|
219
|
-
try:
|
|
220
|
-
result = hook.execute(context)
|
|
221
|
-
execution_time = (time.time() - start_time) * 1000 # ms
|
|
222
|
-
|
|
223
|
-
# Convert HookResult to dict
|
|
224
|
-
return self._success_response({
|
|
225
|
-
"hook_name": hook_name,
|
|
226
|
-
"success": result.success,
|
|
227
|
-
"data": result.data,
|
|
228
|
-
"error": result.error,
|
|
229
|
-
"modified": result.modified,
|
|
230
|
-
"metadata": result.metadata,
|
|
231
|
-
"execution_time_ms": execution_time
|
|
232
|
-
}, request_id)
|
|
233
|
-
|
|
234
|
-
except Exception as e:
|
|
235
|
-
logger.error(f"Hook execution error: {e}")
|
|
236
|
-
logger.error(traceback.format_exc())
|
|
237
|
-
execution_time = (time.time() - start_time) * 1000
|
|
238
|
-
|
|
239
|
-
return self._success_response({
|
|
240
|
-
"hook_name": hook_name,
|
|
241
|
-
"success": False,
|
|
242
|
-
"error": str(e),
|
|
243
|
-
"execution_time_ms": execution_time
|
|
244
|
-
}, request_id)
|
|
245
|
-
|
|
246
|
-
except Exception as e:
|
|
247
|
-
logger.error(f"Unexpected error in _execute_hook: {e}")
|
|
248
|
-
logger.error(traceback.format_exc())
|
|
249
|
-
return self._error_response(
|
|
250
|
-
-32603, "Internal error", request_id,
|
|
251
|
-
str(e)
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
def _success_response(self, result: Any, request_id: Any) -> Dict[str, Any]:
|
|
255
|
-
"""Create a successful JSON-RPC response.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
258
|
-
result: Result data
|
|
259
|
-
request_id: Request ID
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
JSON-RPC response object
|
|
263
|
-
"""
|
|
264
|
-
return {
|
|
265
|
-
"jsonrpc": "2.0",
|
|
266
|
-
"result": result,
|
|
267
|
-
"id": request_id
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
def _error_response(self, code: int, message: str,
|
|
271
|
-
request_id: Any, data: Optional[Any] = None) -> Dict[str, Any]:
|
|
272
|
-
"""Create an error JSON-RPC response.
|
|
273
|
-
|
|
274
|
-
Args:
|
|
275
|
-
code: Error code
|
|
276
|
-
message: Error message
|
|
277
|
-
request_id: Request ID
|
|
278
|
-
data: Optional error data
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
JSON-RPC error response object
|
|
282
|
-
"""
|
|
283
|
-
error = {
|
|
284
|
-
"code": code,
|
|
285
|
-
"message": message
|
|
286
|
-
}
|
|
287
|
-
if data is not None:
|
|
288
|
-
error["data"] = data
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
"jsonrpc": "2.0",
|
|
292
|
-
"error": error,
|
|
293
|
-
"id": request_id
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
def handle_batch(self, requests: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
297
|
-
"""Handle a batch of JSON-RPC requests.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
requests: List of JSON-RPC requests
|
|
301
|
-
|
|
302
|
-
Returns:
|
|
303
|
-
List of JSON-RPC responses
|
|
304
|
-
"""
|
|
305
|
-
if not isinstance(requests, list) or not requests:
|
|
306
|
-
return [self._error_response(
|
|
307
|
-
-32600, "Invalid Request", None,
|
|
308
|
-
"Batch must be a non-empty array"
|
|
309
|
-
)]
|
|
310
|
-
|
|
311
|
-
responses = []
|
|
312
|
-
for request in requests:
|
|
313
|
-
response = self.handle_request(request)
|
|
314
|
-
responses.append(response)
|
|
315
|
-
|
|
316
|
-
return responses
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def main():
|
|
320
|
-
"""Main entry point for hook runner subprocess."""
|
|
321
|
-
runner = JSONRPCHookRunner()
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
# Check for batch mode
|
|
325
|
-
batch_mode = "--batch" in sys.argv
|
|
326
|
-
|
|
327
|
-
# Read JSON-RPC request from stdin
|
|
328
|
-
input_data = sys.stdin.read()
|
|
329
|
-
|
|
330
|
-
try:
|
|
331
|
-
if batch_mode:
|
|
332
|
-
requests = json.loads(input_data)
|
|
333
|
-
responses = runner.handle_batch(requests)
|
|
334
|
-
print(json.dumps(responses))
|
|
335
|
-
else:
|
|
336
|
-
request = json.loads(input_data)
|
|
337
|
-
response = runner.handle_request(request)
|
|
338
|
-
print(json.dumps(response))
|
|
339
|
-
|
|
340
|
-
except json.JSONDecodeError as e:
|
|
341
|
-
error_response = {
|
|
342
|
-
"jsonrpc": "2.0",
|
|
343
|
-
"error": {
|
|
344
|
-
"code": -32700,
|
|
345
|
-
"message": "Parse error",
|
|
346
|
-
"data": str(e)
|
|
347
|
-
},
|
|
348
|
-
"id": None
|
|
349
|
-
}
|
|
350
|
-
print(json.dumps(error_response))
|
|
351
|
-
sys.exit(1)
|
|
352
|
-
|
|
353
|
-
except Exception as e:
|
|
354
|
-
logger.error(f"Fatal error in hook runner: {e}")
|
|
355
|
-
logger.error(traceback.format_exc())
|
|
356
|
-
error_response = {
|
|
357
|
-
"jsonrpc": "2.0",
|
|
358
|
-
"error": {
|
|
359
|
-
"code": -32603,
|
|
360
|
-
"message": "Internal error",
|
|
361
|
-
"data": str(e)
|
|
362
|
-
},
|
|
363
|
-
"id": None
|
|
364
|
-
}
|
|
365
|
-
print(json.dumps(error_response))
|
|
366
|
-
sys.exit(1)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if __name__ == "__main__":
|
|
370
|
-
main()
|