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,370 @@
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()
@@ -0,0 +1,259 @@
1
+ """JSON-RPC based hook executor that runs hooks as subprocess calls."""
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from claude_mpm.hooks.base_hook import HookContext, HookResult, HookType
12
+ from claude_mpm.core.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class JSONRPCError(Exception):
18
+ """JSON-RPC error exception."""
19
+
20
+ def __init__(self, code: int, message: str, data: Optional[Any] = None):
21
+ """Initialize JSON-RPC error.
22
+
23
+ Args:
24
+ code: Error code
25
+ message: Error message
26
+ data: Optional error data
27
+ """
28
+ super().__init__(message)
29
+ self.code = code
30
+ self.message = message
31
+ self.data = data
32
+
33
+
34
+ class JSONRPCHookExecutor:
35
+ """Executes hooks via JSON-RPC subprocess calls."""
36
+
37
+ # JSON-RPC error codes
38
+ PARSE_ERROR = -32700
39
+ INVALID_REQUEST = -32600
40
+ METHOD_NOT_FOUND = -32601
41
+ INVALID_PARAMS = -32602
42
+ INTERNAL_ERROR = -32603
43
+
44
+ def __init__(self, timeout: int = 30):
45
+ """Initialize JSON-RPC hook executor.
46
+
47
+ Args:
48
+ timeout: Timeout for hook execution in seconds
49
+ """
50
+ self.timeout = timeout
51
+ self._hook_runner_path = Path(__file__).parent / "hook_runner.py"
52
+
53
+ def execute_hook(self, hook_name: str, hook_type: HookType,
54
+ context_data: Dict[str, Any],
55
+ metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
56
+ """Execute a hook via JSON-RPC subprocess.
57
+
58
+ Args:
59
+ hook_name: Name of the hook to execute
60
+ hook_type: Type of hook
61
+ context_data: Context data for the hook
62
+ metadata: Optional metadata
63
+
64
+ Returns:
65
+ Execution result dictionary
66
+
67
+ Raises:
68
+ JSONRPCError: If JSON-RPC call fails
69
+ """
70
+ # Create JSON-RPC request
71
+ request = {
72
+ "jsonrpc": "2.0",
73
+ "method": "execute_hook",
74
+ "params": {
75
+ "hook_name": hook_name,
76
+ "hook_type": hook_type.value,
77
+ "context_data": context_data,
78
+ "metadata": metadata or {}
79
+ },
80
+ "id": f"{hook_name}_{int(time.time()*1000)}"
81
+ }
82
+
83
+ try:
84
+ # Execute hook runner subprocess
85
+ result = subprocess.run(
86
+ [sys.executable, str(self._hook_runner_path)],
87
+ input=json.dumps(request),
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=self.timeout
91
+ )
92
+
93
+ if result.returncode != 0:
94
+ logger.error(f"Hook runner failed with code {result.returncode}: {result.stderr}")
95
+ raise JSONRPCError(
96
+ self.INTERNAL_ERROR,
97
+ f"Hook runner process failed: {result.stderr}"
98
+ )
99
+
100
+ # Parse JSON-RPC response
101
+ try:
102
+ response = json.loads(result.stdout)
103
+ except json.JSONDecodeError as e:
104
+ logger.error(f"Failed to parse hook runner output: {result.stdout}")
105
+ raise JSONRPCError(
106
+ self.PARSE_ERROR,
107
+ f"Invalid JSON response: {str(e)}"
108
+ )
109
+
110
+ # Check for JSON-RPC error
111
+ if "error" in response:
112
+ error = response["error"]
113
+ raise JSONRPCError(
114
+ error.get("code", self.INTERNAL_ERROR),
115
+ error.get("message", "Unknown error"),
116
+ error.get("data")
117
+ )
118
+
119
+ # Extract result
120
+ if "result" not in response:
121
+ raise JSONRPCError(
122
+ self.INVALID_REQUEST,
123
+ "Missing result in response"
124
+ )
125
+
126
+ return response["result"]
127
+
128
+ except subprocess.TimeoutExpired:
129
+ logger.error(f"Hook execution timed out after {self.timeout}s")
130
+ raise JSONRPCError(
131
+ self.INTERNAL_ERROR,
132
+ f"Hook execution timed out after {self.timeout} seconds"
133
+ )
134
+ except Exception as e:
135
+ if isinstance(e, JSONRPCError):
136
+ raise
137
+ logger.error(f"Unexpected error executing hook: {e}")
138
+ raise JSONRPCError(
139
+ self.INTERNAL_ERROR,
140
+ f"Unexpected error: {str(e)}"
141
+ )
142
+
143
+ def execute_hooks(self, hook_type: HookType, hook_names: List[str],
144
+ context_data: Dict[str, Any],
145
+ metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
146
+ """Execute multiple hooks of the same type.
147
+
148
+ Args:
149
+ hook_type: Type of hooks to execute
150
+ hook_names: List of hook names to execute
151
+ context_data: Context data for the hooks
152
+ metadata: Optional metadata
153
+
154
+ Returns:
155
+ List of execution results
156
+ """
157
+ results = []
158
+
159
+ for hook_name in hook_names:
160
+ try:
161
+ result = self.execute_hook(
162
+ hook_name=hook_name,
163
+ hook_type=hook_type,
164
+ context_data=context_data,
165
+ metadata=metadata
166
+ )
167
+ results.append(result)
168
+ except JSONRPCError as e:
169
+ # Include error in results but continue with other hooks
170
+ results.append({
171
+ "hook_name": hook_name,
172
+ "success": False,
173
+ "error": e.message,
174
+ "error_code": e.code,
175
+ "error_data": e.data
176
+ })
177
+ logger.error(f"Hook '{hook_name}' failed: {e.message}")
178
+ except Exception as e:
179
+ # Unexpected errors
180
+ results.append({
181
+ "hook_name": hook_name,
182
+ "success": False,
183
+ "error": str(e)
184
+ })
185
+ logger.error(f"Unexpected error in hook '{hook_name}': {e}")
186
+
187
+ return results
188
+
189
+ def batch_execute(self, requests: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
190
+ """Execute multiple hook requests in batch.
191
+
192
+ Args:
193
+ requests: List of hook execution requests
194
+
195
+ Returns:
196
+ List of execution results
197
+ """
198
+ # Create JSON-RPC batch request
199
+ batch_request = []
200
+ for i, req in enumerate(requests):
201
+ batch_request.append({
202
+ "jsonrpc": "2.0",
203
+ "method": "execute_hook",
204
+ "params": req,
205
+ "id": f"batch_{i}_{int(time.time()*1000)}"
206
+ })
207
+
208
+ try:
209
+ # Execute hook runner with batch request
210
+ result = subprocess.run(
211
+ [sys.executable, str(self._hook_runner_path), "--batch"],
212
+ input=json.dumps(batch_request),
213
+ capture_output=True,
214
+ text=True,
215
+ timeout=self.timeout * len(requests) # Scale timeout with batch size
216
+ )
217
+
218
+ if result.returncode != 0:
219
+ logger.error(f"Batch hook runner failed: {result.stderr}")
220
+ return [{
221
+ "success": False,
222
+ "error": f"Batch execution failed: {result.stderr}"
223
+ } for _ in requests]
224
+
225
+ # Parse batch response
226
+ try:
227
+ responses = json.loads(result.stdout)
228
+ except json.JSONDecodeError:
229
+ logger.error(f"Failed to parse batch response: {result.stdout}")
230
+ return [{
231
+ "success": False,
232
+ "error": "Invalid JSON response from batch execution"
233
+ } for _ in requests]
234
+
235
+ # Extract results
236
+ results = []
237
+ for response in responses:
238
+ if "error" in response:
239
+ results.append({
240
+ "success": False,
241
+ "error": response["error"].get("message", "Unknown error")
242
+ })
243
+ else:
244
+ results.append(response.get("result", {"success": False}))
245
+
246
+ return results
247
+
248
+ except subprocess.TimeoutExpired:
249
+ logger.error(f"Batch execution timed out")
250
+ return [{
251
+ "success": False,
252
+ "error": "Batch execution timed out"
253
+ } for _ in requests]
254
+ except Exception as e:
255
+ logger.error(f"Unexpected error in batch execution: {e}")
256
+ return [{
257
+ "success": False,
258
+ "error": f"Unexpected error: {str(e)}"
259
+ } for _ in requests]