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.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- 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]
|