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
|
@@ -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()
|
|
@@ -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
|