claude-mpm 0.3.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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,388 @@
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()
@@ -0,0 +1,223 @@
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()
@@ -0,0 +1,92 @@
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