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,319 @@
|
|
|
1
|
+
"""JSON-RPC based client for executing hooks without HTTP server."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from claude_mpm.hooks.base_hook import (
|
|
10
|
+
BaseHook, HookType,
|
|
11
|
+
SubmitHook, PreDelegationHook, PostDelegationHook, TicketExtractionHook
|
|
12
|
+
)
|
|
13
|
+
from claude_mpm.hooks.json_rpc_executor import JSONRPCHookExecutor, JSONRPCError
|
|
14
|
+
from claude_mpm.core.logger import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JSONRPCHookClient:
|
|
20
|
+
"""Client for executing hooks via JSON-RPC subprocess calls."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, timeout: int = 30):
|
|
23
|
+
"""Initialize JSON-RPC hook client.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
timeout: Request timeout in seconds
|
|
27
|
+
"""
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
self.executor = JSONRPCHookExecutor(timeout=timeout)
|
|
30
|
+
self._hook_registry = {}
|
|
31
|
+
self._hook_types = {}
|
|
32
|
+
self._discover_hooks()
|
|
33
|
+
|
|
34
|
+
def _discover_hooks(self):
|
|
35
|
+
"""Discover available hooks from builtin directory."""
|
|
36
|
+
hooks_dir = Path(__file__).parent / 'builtin'
|
|
37
|
+
if not hooks_dir.exists():
|
|
38
|
+
logger.warning(f"Builtin hooks directory not found: {hooks_dir}")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
for hook_file in hooks_dir.glob('*.py'):
|
|
42
|
+
if hook_file.name.startswith('_'):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Load the module to discover hooks
|
|
47
|
+
module_name = hook_file.stem
|
|
48
|
+
spec = importlib.util.spec_from_file_location(module_name, hook_file)
|
|
49
|
+
if spec and spec.loader:
|
|
50
|
+
module = importlib.util.module_from_spec(spec)
|
|
51
|
+
spec.loader.exec_module(module)
|
|
52
|
+
|
|
53
|
+
# Find hook classes (don't instantiate, just register)
|
|
54
|
+
for name, obj in inspect.getmembers(module):
|
|
55
|
+
if (inspect.isclass(obj) and
|
|
56
|
+
issubclass(obj, BaseHook) and
|
|
57
|
+
obj not in [BaseHook, SubmitHook, PreDelegationHook,
|
|
58
|
+
PostDelegationHook, TicketExtractionHook] and
|
|
59
|
+
not name.startswith('_')):
|
|
60
|
+
# Create temporary instance to get name
|
|
61
|
+
temp_instance = obj()
|
|
62
|
+
hook_name = temp_instance.name
|
|
63
|
+
|
|
64
|
+
# Determine hook type
|
|
65
|
+
if isinstance(temp_instance, SubmitHook):
|
|
66
|
+
hook_type = HookType.SUBMIT
|
|
67
|
+
elif isinstance(temp_instance, PreDelegationHook):
|
|
68
|
+
hook_type = HookType.PRE_DELEGATION
|
|
69
|
+
elif isinstance(temp_instance, PostDelegationHook):
|
|
70
|
+
hook_type = HookType.POST_DELEGATION
|
|
71
|
+
elif isinstance(temp_instance, TicketExtractionHook):
|
|
72
|
+
hook_type = HookType.TICKET_EXTRACTION
|
|
73
|
+
else:
|
|
74
|
+
hook_type = HookType.CUSTOM
|
|
75
|
+
|
|
76
|
+
self._hook_registry[hook_name] = {
|
|
77
|
+
'name': hook_name,
|
|
78
|
+
'type': hook_type,
|
|
79
|
+
'priority': temp_instance.priority,
|
|
80
|
+
'class': name,
|
|
81
|
+
'module': module_name
|
|
82
|
+
}
|
|
83
|
+
self._hook_types[hook_type] = self._hook_types.get(hook_type, [])
|
|
84
|
+
self._hook_types[hook_type].append(hook_name)
|
|
85
|
+
|
|
86
|
+
logger.debug(f"Discovered hook '{hook_name}' of type {hook_type.value}")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to discover hooks from {hook_file}: {e}")
|
|
90
|
+
|
|
91
|
+
def health_check(self) -> Dict[str, Any]:
|
|
92
|
+
"""Check health of hook system.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Health status dictionary
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
hook_count = len(self._hook_registry)
|
|
99
|
+
return {
|
|
100
|
+
'status': 'healthy',
|
|
101
|
+
'hook_count': hook_count,
|
|
102
|
+
'executor': 'json-rpc',
|
|
103
|
+
'discovered_hooks': list(self._hook_registry.keys())
|
|
104
|
+
}
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Health check failed: {e}")
|
|
107
|
+
return {
|
|
108
|
+
'status': 'unhealthy',
|
|
109
|
+
'error': str(e)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
113
|
+
"""List all registered hooks.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary mapping hook types to hook info
|
|
117
|
+
"""
|
|
118
|
+
hooks_by_type = {}
|
|
119
|
+
|
|
120
|
+
for hook_type in HookType:
|
|
121
|
+
hooks_by_type[hook_type.value] = []
|
|
122
|
+
|
|
123
|
+
for hook_name, hook_info in self._hook_registry.items():
|
|
124
|
+
hook_type = hook_info['type']
|
|
125
|
+
hooks_by_type[hook_type.value].append({
|
|
126
|
+
'name': hook_name,
|
|
127
|
+
'priority': hook_info['priority'],
|
|
128
|
+
'enabled': True # All discovered hooks are enabled
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# Sort by priority
|
|
132
|
+
for hook_list in hooks_by_type.values():
|
|
133
|
+
hook_list.sort(key=lambda x: x['priority'])
|
|
134
|
+
|
|
135
|
+
return hooks_by_type
|
|
136
|
+
|
|
137
|
+
def execute_hook(self, hook_type: HookType, context_data: Dict[str, Any],
|
|
138
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
139
|
+
specific_hook: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
140
|
+
"""Execute hooks of a given type.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
hook_type: Type of hooks to execute
|
|
144
|
+
context_data: Data to pass to hooks
|
|
145
|
+
metadata: Optional metadata
|
|
146
|
+
specific_hook: Optional specific hook name to execute
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of execution results
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
if specific_hook:
|
|
153
|
+
# Execute specific hook
|
|
154
|
+
if specific_hook not in self._hook_registry:
|
|
155
|
+
logger.error(f"Hook '{specific_hook}' not found")
|
|
156
|
+
return [{
|
|
157
|
+
'hook_name': specific_hook,
|
|
158
|
+
'success': False,
|
|
159
|
+
'error': f"Hook '{specific_hook}' not found"
|
|
160
|
+
}]
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
result = self.executor.execute_hook(
|
|
164
|
+
hook_name=specific_hook,
|
|
165
|
+
hook_type=hook_type,
|
|
166
|
+
context_data=context_data,
|
|
167
|
+
metadata=metadata
|
|
168
|
+
)
|
|
169
|
+
return [result]
|
|
170
|
+
except JSONRPCError as e:
|
|
171
|
+
return [{
|
|
172
|
+
'hook_name': specific_hook,
|
|
173
|
+
'success': False,
|
|
174
|
+
'error': e.message,
|
|
175
|
+
'error_code': e.code
|
|
176
|
+
}]
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
# Execute all hooks of the given type
|
|
180
|
+
hook_names = self._hook_types.get(hook_type, [])
|
|
181
|
+
if not hook_names:
|
|
182
|
+
logger.debug(f"No hooks registered for type {hook_type.value}")
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
# Sort by priority
|
|
186
|
+
sorted_hooks = sorted(
|
|
187
|
+
hook_names,
|
|
188
|
+
key=lambda name: self._hook_registry[name]['priority']
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return self.executor.execute_hooks(
|
|
192
|
+
hook_type=hook_type,
|
|
193
|
+
hook_names=sorted_hooks,
|
|
194
|
+
context_data=context_data,
|
|
195
|
+
metadata=metadata
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Failed to execute hooks: {e}")
|
|
200
|
+
return [{
|
|
201
|
+
'success': False,
|
|
202
|
+
'error': str(e)
|
|
203
|
+
}]
|
|
204
|
+
|
|
205
|
+
def execute_submit_hook(self, prompt: str, **kwargs) -> List[Dict[str, Any]]:
|
|
206
|
+
"""Execute submit hooks on a user prompt.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
prompt: User prompt to process
|
|
210
|
+
**kwargs: Additional context data
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of execution results
|
|
214
|
+
"""
|
|
215
|
+
context_data = {'prompt': prompt}
|
|
216
|
+
context_data.update(kwargs)
|
|
217
|
+
return self.execute_hook(HookType.SUBMIT, context_data)
|
|
218
|
+
|
|
219
|
+
def execute_pre_delegation_hook(self, agent: str, context: Dict[str, Any],
|
|
220
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
221
|
+
"""Execute pre-delegation hooks.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
agent: Agent being delegated to
|
|
225
|
+
context: Context being passed to agent
|
|
226
|
+
**kwargs: Additional data
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of execution results
|
|
230
|
+
"""
|
|
231
|
+
context_data = {
|
|
232
|
+
'agent': agent,
|
|
233
|
+
'context': context
|
|
234
|
+
}
|
|
235
|
+
context_data.update(kwargs)
|
|
236
|
+
return self.execute_hook(HookType.PRE_DELEGATION, context_data)
|
|
237
|
+
|
|
238
|
+
def execute_post_delegation_hook(self, agent: str, result: Any,
|
|
239
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
240
|
+
"""Execute post-delegation hooks.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
agent: Agent that was delegated to
|
|
244
|
+
result: Result from agent
|
|
245
|
+
**kwargs: Additional data
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of execution results
|
|
249
|
+
"""
|
|
250
|
+
context_data = {
|
|
251
|
+
'agent': agent,
|
|
252
|
+
'result': result
|
|
253
|
+
}
|
|
254
|
+
context_data.update(kwargs)
|
|
255
|
+
return self.execute_hook(HookType.POST_DELEGATION, context_data)
|
|
256
|
+
|
|
257
|
+
def execute_ticket_extraction_hook(self, content: Any,
|
|
258
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
259
|
+
"""Execute ticket extraction hooks.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
content: Content to extract tickets from
|
|
263
|
+
**kwargs: Additional data
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of execution results
|
|
267
|
+
"""
|
|
268
|
+
context_data = {'content': content}
|
|
269
|
+
context_data.update(kwargs)
|
|
270
|
+
return self.execute_hook(HookType.TICKET_EXTRACTION, context_data)
|
|
271
|
+
|
|
272
|
+
def get_modified_data(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
273
|
+
"""Extract modified data from hook results.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
results: Hook execution results
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Combined modified data from all hooks
|
|
280
|
+
"""
|
|
281
|
+
modified_data = {}
|
|
282
|
+
|
|
283
|
+
for result in results:
|
|
284
|
+
if result.get('modified') and result.get('data'):
|
|
285
|
+
modified_data.update(result['data'])
|
|
286
|
+
|
|
287
|
+
return modified_data
|
|
288
|
+
|
|
289
|
+
def get_extracted_tickets(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
290
|
+
"""Extract tickets from hook results.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
results: Hook execution results
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of extracted tickets
|
|
297
|
+
"""
|
|
298
|
+
all_tickets = []
|
|
299
|
+
|
|
300
|
+
for result in results:
|
|
301
|
+
if result.get('success') and 'tickets' in result.get('data', {}):
|
|
302
|
+
tickets = result['data']['tickets']
|
|
303
|
+
if isinstance(tickets, list):
|
|
304
|
+
all_tickets.extend(tickets)
|
|
305
|
+
|
|
306
|
+
return all_tickets
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# Update the convenience function to use JSON-RPC client
|
|
310
|
+
def get_hook_client(base_url: Optional[str] = None) -> JSONRPCHookClient:
|
|
311
|
+
"""Get a hook client instance.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
base_url: Ignored for JSON-RPC client (kept for compatibility)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
JSONRPCHookClient instance
|
|
318
|
+
"""
|
|
319
|
+
return JSONRPCHookClient()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Tool call interceptor for claude-mpm hook system."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from claude_mpm.hooks.base_hook import BaseHook, HookContext, HookType, HookResult
|
|
10
|
+
from claude_mpm.core.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimpleHookRunner:
|
|
16
|
+
"""Simple hook runner for direct hook execution."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the simple hook runner."""
|
|
20
|
+
self._hooks: Dict[HookType, List[BaseHook]] = defaultdict(list)
|
|
21
|
+
self._hook_instances: Dict[str, BaseHook] = {}
|
|
22
|
+
|
|
23
|
+
def register_hook(self, hook: BaseHook, hook_type: Optional[HookType] = None):
|
|
24
|
+
"""Register a hook instance."""
|
|
25
|
+
if hook_type is None:
|
|
26
|
+
hook_type = HookType.CUSTOM
|
|
27
|
+
|
|
28
|
+
if hook.name in self._hook_instances:
|
|
29
|
+
# Remove old instance
|
|
30
|
+
for hook_list in self._hooks.values():
|
|
31
|
+
if self._hook_instances[hook.name] in hook_list:
|
|
32
|
+
hook_list.remove(self._hook_instances[hook.name])
|
|
33
|
+
|
|
34
|
+
self._hooks[hook_type].append(hook)
|
|
35
|
+
self._hook_instances[hook.name] = hook
|
|
36
|
+
self._hooks[hook_type].sort() # Sort by priority
|
|
37
|
+
|
|
38
|
+
async def run_hooks(self, context: HookContext) -> List[Dict[str, Any]]:
|
|
39
|
+
"""Run all hooks for the given context."""
|
|
40
|
+
hooks = [h for h in self._hooks[context.hook_type] if h.enabled]
|
|
41
|
+
results = []
|
|
42
|
+
|
|
43
|
+
for hook in hooks:
|
|
44
|
+
try:
|
|
45
|
+
if hook.validate(context):
|
|
46
|
+
result = hook.execute(context)
|
|
47
|
+
results.append({
|
|
48
|
+
'hook_name': hook.name,
|
|
49
|
+
'success': result.success,
|
|
50
|
+
'data': result.data,
|
|
51
|
+
'error': result.error,
|
|
52
|
+
'modified': result.modified,
|
|
53
|
+
'metadata': result.metadata
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Update context if modified
|
|
57
|
+
if result.modified and result.data:
|
|
58
|
+
context.data.update(result.data)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Hook '{hook.name}' execution failed: {e}")
|
|
61
|
+
results.append({
|
|
62
|
+
'hook_name': hook.name,
|
|
63
|
+
'success': False,
|
|
64
|
+
'error': str(e)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return results
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ToolCallInterceptor:
|
|
71
|
+
"""Intercepts and processes tool calls through the hook system."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, hook_runner: Optional[SimpleHookRunner] = None):
|
|
74
|
+
"""Initialize the tool call interceptor.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
hook_runner: Optional hook runner instance. If not provided, creates a new one.
|
|
78
|
+
"""
|
|
79
|
+
self.hook_runner = hook_runner or SimpleHookRunner()
|
|
80
|
+
|
|
81
|
+
async def intercept_tool_call(self, tool_name: str, parameters: Dict[str, Any],
|
|
82
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
83
|
+
"""Intercept a tool call and run it through the hook system.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tool_name: Name of the tool being called
|
|
87
|
+
parameters: Parameters being passed to the tool
|
|
88
|
+
metadata: Optional metadata for the tool call
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict containing:
|
|
92
|
+
- allowed: Whether the tool call should proceed
|
|
93
|
+
- parameters: Potentially modified parameters
|
|
94
|
+
- error: Error message if not allowed
|
|
95
|
+
- metadata: Additional metadata from hooks
|
|
96
|
+
"""
|
|
97
|
+
# Create hook context for tool call interception
|
|
98
|
+
context = HookContext(
|
|
99
|
+
hook_type=HookType.CUSTOM,
|
|
100
|
+
data={
|
|
101
|
+
'tool_name': tool_name,
|
|
102
|
+
'parameters': parameters.copy() # Copy to avoid modifying original
|
|
103
|
+
},
|
|
104
|
+
metadata=metadata or {},
|
|
105
|
+
timestamp=datetime.now()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Run hooks
|
|
109
|
+
results = await self.hook_runner.run_hooks(context)
|
|
110
|
+
|
|
111
|
+
# Process results
|
|
112
|
+
allowed = True
|
|
113
|
+
modified_params = parameters
|
|
114
|
+
errors = []
|
|
115
|
+
hook_metadata = {}
|
|
116
|
+
|
|
117
|
+
for result in results:
|
|
118
|
+
if not result.get('success', True):
|
|
119
|
+
allowed = False
|
|
120
|
+
if result.get('error'):
|
|
121
|
+
errors.append(f"[{result.get('hook_name', 'Unknown')}] {result.get('error')}")
|
|
122
|
+
|
|
123
|
+
if result.get('modified') and result.get('data'):
|
|
124
|
+
# Update parameters if modified
|
|
125
|
+
if 'parameters' in result.get('data', {}):
|
|
126
|
+
modified_params = result['data']['parameters']
|
|
127
|
+
|
|
128
|
+
if result.get('metadata'):
|
|
129
|
+
hook_metadata.update(result['metadata'])
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
'allowed': allowed,
|
|
133
|
+
'parameters': modified_params,
|
|
134
|
+
'error': '\n'.join(errors) if errors else None,
|
|
135
|
+
'metadata': hook_metadata
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def intercept_tool_call_sync(self, tool_name: str, parameters: Dict[str, Any],
|
|
139
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
140
|
+
"""Synchronous version of intercept_tool_call."""
|
|
141
|
+
loop = asyncio.new_event_loop()
|
|
142
|
+
asyncio.set_event_loop(loop)
|
|
143
|
+
try:
|
|
144
|
+
return loop.run_until_complete(
|
|
145
|
+
self.intercept_tool_call(tool_name, parameters, metadata)
|
|
146
|
+
)
|
|
147
|
+
finally:
|
|
148
|
+
loop.close()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ToolCallHookIntegration:
|
|
152
|
+
"""Integration helper for adding tool call interception to existing systems."""
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def wrap_tool_executor(original_executor, interceptor: ToolCallInterceptor):
|
|
156
|
+
"""Wrap an existing tool executor with hook interception.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
original_executor: The original tool execution function
|
|
160
|
+
interceptor: The tool call interceptor instance
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Wrapped executor function
|
|
164
|
+
"""
|
|
165
|
+
async def wrapped_executor(tool_name: str, parameters: Dict[str, Any], **kwargs):
|
|
166
|
+
# Intercept the tool call
|
|
167
|
+
interception_result = await interceptor.intercept_tool_call(
|
|
168
|
+
tool_name, parameters, kwargs.get('metadata')
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Check if allowed
|
|
172
|
+
if not interception_result['allowed']:
|
|
173
|
+
raise ValueError(f"Tool call blocked: {interception_result['error']}")
|
|
174
|
+
|
|
175
|
+
# Execute with potentially modified parameters
|
|
176
|
+
return await original_executor(
|
|
177
|
+
tool_name,
|
|
178
|
+
interception_result['parameters'],
|
|
179
|
+
**kwargs
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return wrapped_executor
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def create_tool_call_validator(valid_tools: List[str], interceptor: ToolCallInterceptor):
|
|
186
|
+
"""Create a tool call validator that uses the hook system.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
valid_tools: List of valid tool names
|
|
190
|
+
interceptor: The tool call interceptor instance
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Validator function
|
|
194
|
+
"""
|
|
195
|
+
def validator(tool_name: str, parameters: Dict[str, Any]) -> bool:
|
|
196
|
+
# Basic validation
|
|
197
|
+
if tool_name not in valid_tools:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Hook-based validation
|
|
201
|
+
result = interceptor.intercept_tool_call_sync(tool_name, parameters)
|
|
202
|
+
return result['allowed']
|
|
203
|
+
|
|
204
|
+
return validator
|