claude-mpm 1.0.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/_version.py +4 -2
- claude_mpm/agents/INSTRUCTIONS.md +117 -312
- claude_mpm/agents/__init__.py +2 -2
- claude_mpm/agents/agent-template.yaml +83 -0
- claude_mpm/agents/agent_loader.py +192 -310
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/base_agent_loader.py +10 -15
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +46 -0
- claude_mpm/agents/templates/{engineer_agent.json → backup/engineer_agent_20250726_234551.json} +1 -1
- claude_mpm/agents/templates/data_engineer.json +107 -0
- claude_mpm/agents/templates/documentation.json +106 -0
- claude_mpm/agents/templates/engineer.json +110 -0
- claude_mpm/agents/templates/ops.json +106 -0
- claude_mpm/agents/templates/qa.json +106 -0
- claude_mpm/agents/templates/research.json +107 -0
- claude_mpm/agents/templates/security.json +105 -0
- claude_mpm/agents/templates/version_control.json +103 -0
- claude_mpm/cli.py +41 -47
- claude_mpm/cli_enhancements.py +297 -0
- claude_mpm/core/factories.py +1 -46
- claude_mpm/core/service_registry.py +0 -8
- claude_mpm/core/simple_runner.py +43 -0
- claude_mpm/generators/__init__.py +5 -0
- claude_mpm/generators/agent_profile_generator.py +137 -0
- claude_mpm/hooks/README.md +75 -221
- claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
- claude_mpm/hooks/claude_hooks/__init__.py +5 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
- claude_mpm/hooks/validation_hooks.py +181 -0
- claude_mpm/schemas/agent_schema.json +328 -0
- claude_mpm/services/agent_management_service.py +4 -4
- claude_mpm/services/agent_profile_loader.py +1 -1
- claude_mpm/services/agent_registry.py +0 -1
- claude_mpm/services/base_agent_manager.py +3 -3
- claude_mpm/utils/error_handler.py +247 -0
- claude_mpm/validation/__init__.py +5 -0
- claude_mpm/validation/agent_validator.py +302 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/METADATA +133 -22
- {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/RECORD +49 -37
- claude_mpm/agents/templates/data_engineer_agent.json +0 -46
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +0 -374
- claude_mpm/config/hook_config.py +0 -42
- claude_mpm/hooks/hook_client.py +0 -264
- claude_mpm/hooks/hook_runner.py +0 -370
- claude_mpm/hooks/json_rpc_executor.py +0 -259
- claude_mpm/hooks/json_rpc_hook_client.py +0 -319
- claude_mpm/services/hook_service.py +0 -388
- claude_mpm/services/hook_service_manager.py +0 -223
- claude_mpm/services/json_rpc_hook_manager.py +0 -92
- /claude_mpm/agents/templates/{documentation_agent.json → backup/documentation_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{ops_agent.json → backup/ops_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{qa_agent.json → backup/qa_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{research_agent.json → backup/research_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{security_agent.json → backup/security_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{version_control_agent.json → backup/version_control_agent_20250726_234551.json} +0 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/WHEEL +0 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unified hook handler for Claude Code integration.
|
|
3
|
+
|
|
4
|
+
This script is called by hook_wrapper.sh, which is the shell script
|
|
5
|
+
that gets installed in ~/.claude/settings.json. The wrapper handles
|
|
6
|
+
environment setup and then executes this Python handler.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Add src to path for imports
|
|
18
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
19
|
+
sys.path.insert(0, str(project_root / 'src'))
|
|
20
|
+
|
|
21
|
+
from claude_mpm.core.logger import get_logger, setup_logging, LogLevel
|
|
22
|
+
|
|
23
|
+
# Don't initialize global logging here - we'll do it per-project
|
|
24
|
+
logger = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeHookHandler:
|
|
28
|
+
"""Handler for all Claude Code hook events."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.event = None
|
|
32
|
+
self.hook_type = None
|
|
33
|
+
|
|
34
|
+
# Available MPM arguments
|
|
35
|
+
self.mpm_args = {
|
|
36
|
+
'status': 'Show claude-mpm system status',
|
|
37
|
+
# Add more arguments here as they're implemented
|
|
38
|
+
# 'config': 'Configure claude-mpm settings',
|
|
39
|
+
# 'debug': 'Toggle debug mode',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def handle(self):
|
|
43
|
+
"""Main entry point for hook handling."""
|
|
44
|
+
global logger
|
|
45
|
+
try:
|
|
46
|
+
# Quick debug log to file
|
|
47
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
48
|
+
f.write(f"[{datetime.now().isoformat()}] Hook called\n")
|
|
49
|
+
|
|
50
|
+
# Read event from stdin
|
|
51
|
+
event_data = sys.stdin.read()
|
|
52
|
+
self.event = json.loads(event_data)
|
|
53
|
+
self.hook_type = self.event.get('hook_event_name', 'unknown')
|
|
54
|
+
|
|
55
|
+
# Get the working directory from the event
|
|
56
|
+
cwd = self.event.get('cwd', os.getcwd())
|
|
57
|
+
project_dir = Path(cwd)
|
|
58
|
+
|
|
59
|
+
# Initialize project-specific logging
|
|
60
|
+
log_dir = project_dir / '.claude-mpm' / 'logs'
|
|
61
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
# Set up logging for this specific project
|
|
64
|
+
# Use a single log file per day per project
|
|
65
|
+
log_level = os.environ.get('CLAUDE_MPM_LOG_LEVEL', 'INFO')
|
|
66
|
+
log_file = log_dir / f"hooks_{datetime.now().strftime('%Y%m%d')}.log"
|
|
67
|
+
|
|
68
|
+
# Only set up logging if we haven't already for this project
|
|
69
|
+
logger_name = f"claude_mpm_hooks_{project_dir.name}"
|
|
70
|
+
if not logging.getLogger(logger_name).handlers:
|
|
71
|
+
logger = setup_logging(
|
|
72
|
+
name=logger_name,
|
|
73
|
+
level=log_level,
|
|
74
|
+
log_dir=log_dir,
|
|
75
|
+
log_file=log_file
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
logger = logging.getLogger(logger_name)
|
|
79
|
+
|
|
80
|
+
# Log more details about the hook type
|
|
81
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
82
|
+
f.write(f"[{datetime.now().isoformat()}] Hook type: {self.hook_type}\n")
|
|
83
|
+
f.write(f"[{datetime.now().isoformat()}] Project: {project_dir}\n")
|
|
84
|
+
|
|
85
|
+
# Log the prompt if it's UserPromptSubmit
|
|
86
|
+
if self.hook_type == 'UserPromptSubmit':
|
|
87
|
+
prompt = self.event.get('prompt', '')
|
|
88
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
89
|
+
f.write(f"[{datetime.now().isoformat()}] Prompt: {prompt}\n")
|
|
90
|
+
|
|
91
|
+
# Log the event if DEBUG logging is enabled
|
|
92
|
+
self._log_event()
|
|
93
|
+
|
|
94
|
+
# Route to appropriate handler
|
|
95
|
+
if self.hook_type == 'UserPromptSubmit':
|
|
96
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
97
|
+
f.write(f"[{datetime.now().isoformat()}] About to call _handle_user_prompt_submit\n")
|
|
98
|
+
return self._handle_user_prompt_submit()
|
|
99
|
+
elif self.hook_type == 'PreToolUse':
|
|
100
|
+
return self._handle_pre_tool_use()
|
|
101
|
+
elif self.hook_type == 'PostToolUse':
|
|
102
|
+
return self._handle_post_tool_use()
|
|
103
|
+
elif self.hook_type == 'Stop':
|
|
104
|
+
return self._handle_stop()
|
|
105
|
+
elif self.hook_type == 'SubagentStop':
|
|
106
|
+
return self._handle_subagent_stop()
|
|
107
|
+
else:
|
|
108
|
+
logger.debug(f"Unknown hook type: {self.hook_type}")
|
|
109
|
+
return self._continue()
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
113
|
+
f.write(f"[{datetime.now().isoformat()}] Hook handler error: {e}\n")
|
|
114
|
+
import traceback
|
|
115
|
+
f.write(traceback.format_exc())
|
|
116
|
+
if logger:
|
|
117
|
+
logger.error(f"Hook handler error: {e}")
|
|
118
|
+
return self._continue()
|
|
119
|
+
|
|
120
|
+
def _log_event(self):
|
|
121
|
+
"""Log the event details if DEBUG logging is enabled."""
|
|
122
|
+
global logger
|
|
123
|
+
if not logger:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Check if DEBUG logging is enabled
|
|
127
|
+
# logger.level might be an int or LogLevel enum
|
|
128
|
+
try:
|
|
129
|
+
if hasattr(logger.level, 'value'):
|
|
130
|
+
debug_enabled = logger.level.value <= LogLevel.DEBUG.value
|
|
131
|
+
else:
|
|
132
|
+
# It's an int, compare with the DEBUG level value (10)
|
|
133
|
+
debug_enabled = logger.level <= 10
|
|
134
|
+
except:
|
|
135
|
+
# If comparison fails, assume debug is disabled
|
|
136
|
+
debug_enabled = False
|
|
137
|
+
|
|
138
|
+
# Always log hook events at INFO level so they appear in the logs
|
|
139
|
+
session_id = self.event.get('session_id', 'unknown')
|
|
140
|
+
cwd = self.event.get('cwd', 'unknown')
|
|
141
|
+
|
|
142
|
+
logger.info(f"Claude Code hook event: {self.hook_type} (session: {session_id[:8] if session_id != 'unknown' else 'unknown'})")
|
|
143
|
+
|
|
144
|
+
if debug_enabled:
|
|
145
|
+
logger.debug(f"Event in directory: {cwd}")
|
|
146
|
+
logger.debug(f"Event data: {json.dumps(self.event, indent=2)}")
|
|
147
|
+
|
|
148
|
+
# Log specific details based on hook type
|
|
149
|
+
if self.hook_type == 'UserPromptSubmit':
|
|
150
|
+
prompt = self.event.get('prompt', '')
|
|
151
|
+
# Don't log full agent system prompts
|
|
152
|
+
if prompt.startswith('You are Claude Code running in Claude MPM'):
|
|
153
|
+
logger.info("UserPromptSubmit: System prompt for agent delegation")
|
|
154
|
+
else:
|
|
155
|
+
logger.info(f"UserPromptSubmit: {prompt[:100]}..." if len(prompt) > 100 else f"UserPromptSubmit: {prompt}")
|
|
156
|
+
elif self.hook_type == 'PreToolUse':
|
|
157
|
+
tool_name = self.event.get('tool_name', '')
|
|
158
|
+
logger.info(f"PreToolUse: {tool_name}")
|
|
159
|
+
if debug_enabled:
|
|
160
|
+
tool_input = self.event.get('tool_input', {})
|
|
161
|
+
logger.debug(f"Tool input: {json.dumps(tool_input, indent=2)}")
|
|
162
|
+
elif self.hook_type == 'PostToolUse':
|
|
163
|
+
tool_name = self.event.get('tool_name', '')
|
|
164
|
+
exit_code = self.event.get('exit_code', 'N/A')
|
|
165
|
+
logger.info(f"PostToolUse: {tool_name} (exit code: {exit_code})")
|
|
166
|
+
if debug_enabled:
|
|
167
|
+
tool_output = self.event.get('tool_output', '')
|
|
168
|
+
logger.debug(f"Tool output: {tool_output[:200]}..." if len(str(tool_output)) > 200 else f"Tool output: {tool_output}")
|
|
169
|
+
elif self.hook_type == 'Stop':
|
|
170
|
+
reason = self.event.get('reason', 'unknown')
|
|
171
|
+
timestamp = datetime.now().isoformat()
|
|
172
|
+
logger.info(f"Stop event: reason={reason} at {timestamp}")
|
|
173
|
+
elif self.hook_type == 'SubagentStop':
|
|
174
|
+
agent_type = self.event.get('agent_type', 'unknown')
|
|
175
|
+
agent_id = self.event.get('agent_id', 'unknown')
|
|
176
|
+
reason = self.event.get('reason', 'unknown')
|
|
177
|
+
timestamp = datetime.now().isoformat()
|
|
178
|
+
logger.info(f"SubagentStop: agent_type={agent_type}, agent_id={agent_id}, reason={reason} at {timestamp}")
|
|
179
|
+
|
|
180
|
+
def _handle_user_prompt_submit(self):
|
|
181
|
+
"""Handle UserPromptSubmit events."""
|
|
182
|
+
try:
|
|
183
|
+
prompt = self.event.get('prompt', '').strip()
|
|
184
|
+
|
|
185
|
+
# Debug log
|
|
186
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
187
|
+
f.write(f"[{datetime.now().isoformat()}] UserPromptSubmit - Checking prompt: '{prompt}'\n")
|
|
188
|
+
|
|
189
|
+
# Check if this is the /mpm command
|
|
190
|
+
if prompt == '/mpm' or prompt.startswith('/mpm '):
|
|
191
|
+
# Parse arguments
|
|
192
|
+
parts = prompt.split(maxsplit=1)
|
|
193
|
+
arg = parts[1] if len(parts) > 1 else ''
|
|
194
|
+
|
|
195
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
196
|
+
f.write(f"[{datetime.now().isoformat()}] MPM command detected, arg: '{arg}'\n")
|
|
197
|
+
|
|
198
|
+
# Route based on argument
|
|
199
|
+
if arg == 'status' or arg.startswith('status '):
|
|
200
|
+
# Extract status args if any
|
|
201
|
+
status_args = arg[6:].strip() if arg.startswith('status ') else ''
|
|
202
|
+
return self._handle_mpm_status(status_args)
|
|
203
|
+
else:
|
|
204
|
+
# Show help for empty or unknown argument
|
|
205
|
+
return self._handle_mpm_help(arg)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
with open('/tmp/claude-mpm-hook.log', 'a') as f:
|
|
209
|
+
f.write(f"[{datetime.now().isoformat()}] Error in _handle_user_prompt_submit: {e}\n")
|
|
210
|
+
import traceback
|
|
211
|
+
f.write(traceback.format_exc())
|
|
212
|
+
|
|
213
|
+
# For now, let everything else pass through
|
|
214
|
+
return self._continue()
|
|
215
|
+
|
|
216
|
+
def _handle_pre_tool_use(self):
|
|
217
|
+
"""Handle PreToolUse events."""
|
|
218
|
+
# For now, just log and continue
|
|
219
|
+
return self._continue()
|
|
220
|
+
|
|
221
|
+
def _handle_post_tool_use(self):
|
|
222
|
+
"""Handle PostToolUse events."""
|
|
223
|
+
# For now, just log and continue
|
|
224
|
+
return self._continue()
|
|
225
|
+
|
|
226
|
+
def _handle_stop(self):
|
|
227
|
+
"""Handle Stop events."""
|
|
228
|
+
# Log the stop event and continue
|
|
229
|
+
return self._continue()
|
|
230
|
+
|
|
231
|
+
def _handle_subagent_stop(self):
|
|
232
|
+
"""Handle SubagentStop events."""
|
|
233
|
+
# Log the subagent stop event and continue
|
|
234
|
+
return self._continue()
|
|
235
|
+
|
|
236
|
+
def _handle_mpm_status(self, args=None):
|
|
237
|
+
"""Handle the /mpm:status command."""
|
|
238
|
+
# Parse arguments if provided
|
|
239
|
+
verbose = False
|
|
240
|
+
if args:
|
|
241
|
+
verbose = '--verbose' in args or '-v' in args
|
|
242
|
+
|
|
243
|
+
# Gather system information
|
|
244
|
+
# Handle logger.level which might be int or LogLevel enum
|
|
245
|
+
if hasattr(logger.level, 'name'):
|
|
246
|
+
log_level_name = logger.level.name
|
|
247
|
+
else:
|
|
248
|
+
# It's an int, map it to name
|
|
249
|
+
level_map = {
|
|
250
|
+
0: 'NOTSET',
|
|
251
|
+
10: 'DEBUG',
|
|
252
|
+
20: 'INFO',
|
|
253
|
+
30: 'WARNING',
|
|
254
|
+
40: 'ERROR',
|
|
255
|
+
50: 'CRITICAL'
|
|
256
|
+
}
|
|
257
|
+
log_level_name = level_map.get(logger.level, f"CUSTOM({logger.level})")
|
|
258
|
+
|
|
259
|
+
status_info = {
|
|
260
|
+
'claude_mpm_version': self._get_version(),
|
|
261
|
+
'python_version': sys.version.split()[0],
|
|
262
|
+
'project_root': str(project_root) if project_root.name != 'src' else str(project_root.parent),
|
|
263
|
+
'logging_level': log_level_name,
|
|
264
|
+
'hook_handler': 'claude_mpm.hooks.claude_hooks.hook_handler',
|
|
265
|
+
'environment': {
|
|
266
|
+
'CLAUDE_PROJECT_DIR': os.environ.get('CLAUDE_PROJECT_DIR', 'not set'),
|
|
267
|
+
'PYTHONPATH': os.environ.get('PYTHONPATH', 'not set'),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Add verbose information if requested
|
|
272
|
+
if verbose:
|
|
273
|
+
status_info['hooks_configured'] = {
|
|
274
|
+
'UserPromptSubmit': 'Active',
|
|
275
|
+
'PreToolUse': 'Active',
|
|
276
|
+
'PostToolUse': 'Active'
|
|
277
|
+
}
|
|
278
|
+
status_info['available_arguments'] = list(self.mpm_args.keys())
|
|
279
|
+
|
|
280
|
+
# Format output
|
|
281
|
+
output = self._format_status_output(status_info, verbose)
|
|
282
|
+
|
|
283
|
+
# Block LLM processing and return our output
|
|
284
|
+
print(output, file=sys.stderr)
|
|
285
|
+
sys.exit(2)
|
|
286
|
+
|
|
287
|
+
def _get_version(self):
|
|
288
|
+
"""Get claude-mpm version."""
|
|
289
|
+
try:
|
|
290
|
+
# First try to read from VERSION file in project root
|
|
291
|
+
version_file = project_root.parent / 'VERSION'
|
|
292
|
+
if not version_file.exists():
|
|
293
|
+
# Try one more level up
|
|
294
|
+
version_file = project_root.parent.parent / 'VERSION'
|
|
295
|
+
|
|
296
|
+
if version_file.exists():
|
|
297
|
+
with open(version_file, 'r') as f:
|
|
298
|
+
version = f.read().strip()
|
|
299
|
+
# Return just the base version for cleaner display
|
|
300
|
+
# e.g., "1.0.2.dev1+g4ecadd4.d20250726" -> "1.0.2.dev1"
|
|
301
|
+
if '+' in version:
|
|
302
|
+
version = version.split('+')[0]
|
|
303
|
+
return version
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# Fallback to trying import
|
|
309
|
+
from claude_mpm import __version__
|
|
310
|
+
return __version__
|
|
311
|
+
except:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
return 'unknown'
|
|
315
|
+
|
|
316
|
+
def _format_status_output(self, info, verbose=False):
|
|
317
|
+
"""Format status information for display."""
|
|
318
|
+
# Use same colors as help screen
|
|
319
|
+
CYAN = '\033[96m' # Bright cyan
|
|
320
|
+
GREEN = '\033[92m' # Green (works in help)
|
|
321
|
+
BOLD = '\033[1m'
|
|
322
|
+
RESET = '\033[0m'
|
|
323
|
+
DIM = '\033[2m'
|
|
324
|
+
|
|
325
|
+
output = f"\n{DIM}{'─' * 60}{RESET}\n"
|
|
326
|
+
output += f"{CYAN}{BOLD}🔧 Claude MPM Status{RESET}\n"
|
|
327
|
+
output += f"{DIM}{'─' * 60}{RESET}\n\n"
|
|
328
|
+
|
|
329
|
+
output += f"{GREEN}Version:{RESET} {info['claude_mpm_version']}\n"
|
|
330
|
+
output += f"{GREEN}Python:{RESET} {info['python_version']}\n"
|
|
331
|
+
output += f"{GREEN}Project Root:{RESET} {info['project_root']}\n"
|
|
332
|
+
output += f"{GREEN}Logging Level:{RESET} {info['logging_level']}\n"
|
|
333
|
+
output += f"{GREEN}Hook Handler:{RESET} {info['hook_handler']}\n"
|
|
334
|
+
|
|
335
|
+
output += f"\n{CYAN}{BOLD}Environment:{RESET}\n"
|
|
336
|
+
for key, value in info['environment'].items():
|
|
337
|
+
output += f"{GREEN} {key}: {value}{RESET}\n"
|
|
338
|
+
|
|
339
|
+
if verbose:
|
|
340
|
+
output += f"\n{CYAN}{BOLD}Hooks Configured:{RESET}\n"
|
|
341
|
+
for hook, status in info.get('hooks_configured', {}).items():
|
|
342
|
+
output += f"{GREEN} {hook}: {status}{RESET}\n"
|
|
343
|
+
|
|
344
|
+
output += f"\n{CYAN}{BOLD}Available Arguments:{RESET}\n"
|
|
345
|
+
for arg in info.get('available_arguments', []):
|
|
346
|
+
output += f"{GREEN} /mpm {arg}{RESET}\n"
|
|
347
|
+
|
|
348
|
+
output += f"\n{DIM}{'─' * 60}{RESET}"
|
|
349
|
+
|
|
350
|
+
return output
|
|
351
|
+
|
|
352
|
+
def _handle_mpm_help(self, unknown_arg=None):
|
|
353
|
+
"""Show help for MPM commands."""
|
|
354
|
+
# ANSI colors
|
|
355
|
+
CYAN = '\033[96m'
|
|
356
|
+
RED = '\033[91m'
|
|
357
|
+
GREEN = '\033[92m'
|
|
358
|
+
DIM = '\033[2m'
|
|
359
|
+
RESET = '\033[0m'
|
|
360
|
+
BOLD = '\033[1m'
|
|
361
|
+
|
|
362
|
+
output = f"\n{DIM}{'─' * 60}{RESET}\n"
|
|
363
|
+
output += f"{CYAN}{BOLD}🔧 Claude MPM Management{RESET}\n"
|
|
364
|
+
output += f"{DIM}{'─' * 60}{RESET}\n\n"
|
|
365
|
+
|
|
366
|
+
if unknown_arg:
|
|
367
|
+
output += f"{RED}Unknown argument: {unknown_arg}{RESET}\n\n"
|
|
368
|
+
|
|
369
|
+
output += f"{GREEN}Usage:{RESET} /mpm [argument]\n\n"
|
|
370
|
+
output += f"{GREEN}Available arguments:{RESET}\n"
|
|
371
|
+
for arg, desc in self.mpm_args.items():
|
|
372
|
+
output += f" {arg:<12} - {desc}\n"
|
|
373
|
+
|
|
374
|
+
output += f"\n{GREEN}Examples:{RESET}\n"
|
|
375
|
+
output += f" /mpm - Show this help\n"
|
|
376
|
+
output += f" /mpm status - Show system status\n"
|
|
377
|
+
output += f" /mpm status --verbose - Show detailed status\n"
|
|
378
|
+
|
|
379
|
+
output += f"\n{DIM}{'─' * 60}{RESET}"
|
|
380
|
+
|
|
381
|
+
# Block LLM processing and return our output
|
|
382
|
+
print(output, file=sys.stderr)
|
|
383
|
+
sys.exit(2)
|
|
384
|
+
|
|
385
|
+
def _continue(self):
|
|
386
|
+
"""Return continue response to let prompt pass through."""
|
|
387
|
+
response = {"action": "continue"}
|
|
388
|
+
print(json.dumps(response))
|
|
389
|
+
sys.exit(0)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def main():
|
|
393
|
+
"""Main entry point."""
|
|
394
|
+
handler = ClaudeHookHandler()
|
|
395
|
+
handler.handle()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code hook wrapper for claude-mpm
|
|
3
|
+
|
|
4
|
+
# Debug log (optional - comment out in production)
|
|
5
|
+
# echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Wrapper called with args: $@" >> /tmp/hook-wrapper.log
|
|
6
|
+
|
|
7
|
+
# Get the directory where this script is located
|
|
8
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
9
|
+
|
|
10
|
+
# Detect if we're in a development environment or installed package
|
|
11
|
+
if [ -d "$SCRIPT_DIR/../../../../venv" ]; then
|
|
12
|
+
# Development environment - script is in src/claude_mpm/hooks/claude_hooks/
|
|
13
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../../../.." && pwd )"
|
|
14
|
+
PYTHON_CMD="python"
|
|
15
|
+
|
|
16
|
+
# Activate the virtual environment if it exists
|
|
17
|
+
if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then
|
|
18
|
+
source "$PROJECT_ROOT/venv/bin/activate"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Set PYTHONPATH for development
|
|
22
|
+
export PYTHONPATH="$PROJECT_ROOT/src:$PYTHONPATH"
|
|
23
|
+
else
|
|
24
|
+
# Installed package - use system Python and installed claude_mpm
|
|
25
|
+
PYTHON_CMD="python3"
|
|
26
|
+
|
|
27
|
+
# Try to detect if we're in a virtual environment
|
|
28
|
+
if [ -n "$VIRTUAL_ENV" ]; then
|
|
29
|
+
PYTHON_CMD="$VIRTUAL_ENV/bin/python"
|
|
30
|
+
elif command -v python3 &> /dev/null; then
|
|
31
|
+
PYTHON_CMD="python3"
|
|
32
|
+
elif command -v python &> /dev/null; then
|
|
33
|
+
PYTHON_CMD="python"
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Check if we should use DEBUG logging
|
|
38
|
+
if [[ " $* " =~ " --logging DEBUG " ]] || [[ " $* " =~ " --debug " ]]; then
|
|
39
|
+
export CLAUDE_MPM_LOG_LEVEL="DEBUG"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Debug log (optional)
|
|
43
|
+
# echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/hook-wrapper.log
|
|
44
|
+
# echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Running: $PYTHON_CMD $SCRIPT_DIR/hook_handler.py" >> /tmp/hook-wrapper.log
|
|
45
|
+
|
|
46
|
+
# Run the Python hook handler
|
|
47
|
+
exec "$PYTHON_CMD" "$SCRIPT_DIR/hook_handler.py" "$@"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation hooks for claude-mpm operations.
|
|
3
|
+
|
|
4
|
+
Inspired by awesome-claude-code's pre-push validation approach.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Any, List, Optional, Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from claude_mpm.validation import AgentValidator, ValidationResult
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationHooks:
|
|
18
|
+
"""Manages validation hooks for various operations."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize validation hooks."""
|
|
22
|
+
self.pre_load_hooks: List[Callable] = []
|
|
23
|
+
self.post_load_hooks: List[Callable] = []
|
|
24
|
+
self.pre_execute_hooks: List[Callable] = []
|
|
25
|
+
self.validator = AgentValidator()
|
|
26
|
+
|
|
27
|
+
def register_pre_load_hook(self, hook: Callable) -> None:
|
|
28
|
+
"""Register a hook to run before loading an agent."""
|
|
29
|
+
self.pre_load_hooks.append(hook)
|
|
30
|
+
|
|
31
|
+
def register_post_load_hook(self, hook: Callable) -> None:
|
|
32
|
+
"""Register a hook to run after loading an agent."""
|
|
33
|
+
self.post_load_hooks.append(hook)
|
|
34
|
+
|
|
35
|
+
def register_pre_execute_hook(self, hook: Callable) -> None:
|
|
36
|
+
"""Register a hook to run before executing an agent."""
|
|
37
|
+
self.pre_execute_hooks.append(hook)
|
|
38
|
+
|
|
39
|
+
async def run_pre_load_validation(self, profile_path: Path) -> ValidationResult:
|
|
40
|
+
"""Run validation before loading an agent profile."""
|
|
41
|
+
logger.info(f"Running pre-load validation for {profile_path}")
|
|
42
|
+
|
|
43
|
+
# Run basic file validation
|
|
44
|
+
if not profile_path.exists():
|
|
45
|
+
result = ValidationResult(is_valid=False)
|
|
46
|
+
result.errors.append(f"Profile file not found: {profile_path}")
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
# Validate profile structure
|
|
50
|
+
result = self.validator.validate_profile(profile_path)
|
|
51
|
+
|
|
52
|
+
# Run custom pre-load hooks
|
|
53
|
+
for hook in self.pre_load_hooks:
|
|
54
|
+
try:
|
|
55
|
+
hook_result = await hook(profile_path)
|
|
56
|
+
if hasattr(hook_result, 'errors'):
|
|
57
|
+
result.errors.extend(hook_result.errors)
|
|
58
|
+
if hook_result.errors:
|
|
59
|
+
result.is_valid = False
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Pre-load hook failed: {e}")
|
|
62
|
+
result.warnings.append(f"Pre-load hook failed: {str(e)}")
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
async def run_post_load_validation(self, agent_config: Dict[str, Any]) -> ValidationResult:
|
|
67
|
+
"""Run validation after loading an agent."""
|
|
68
|
+
result = ValidationResult(is_valid=True)
|
|
69
|
+
|
|
70
|
+
# Run custom post-load hooks
|
|
71
|
+
for hook in self.post_load_hooks:
|
|
72
|
+
try:
|
|
73
|
+
hook_result = await hook(agent_config)
|
|
74
|
+
if hasattr(hook_result, 'warnings'):
|
|
75
|
+
result.warnings.extend(hook_result.warnings)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Post-load hook failed: {e}")
|
|
78
|
+
result.warnings.append(f"Post-load hook failed: {str(e)}")
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
async def run_pre_execute_validation(self, agent_name: str, task: str) -> ValidationResult:
|
|
83
|
+
"""Run validation before executing an agent task."""
|
|
84
|
+
result = ValidationResult(is_valid=True)
|
|
85
|
+
|
|
86
|
+
# Validate task format
|
|
87
|
+
if not task or not task.strip():
|
|
88
|
+
result.errors.append("Task cannot be empty")
|
|
89
|
+
result.is_valid = False
|
|
90
|
+
|
|
91
|
+
# Check task length
|
|
92
|
+
if len(task) > 10000:
|
|
93
|
+
result.warnings.append("Task is very long, consider breaking it down")
|
|
94
|
+
|
|
95
|
+
# Run custom pre-execute hooks
|
|
96
|
+
for hook in self.pre_execute_hooks:
|
|
97
|
+
try:
|
|
98
|
+
hook_result = await hook(agent_name, task)
|
|
99
|
+
if hasattr(hook_result, 'errors') and hook_result.errors:
|
|
100
|
+
result.errors.extend(hook_result.errors)
|
|
101
|
+
result.is_valid = False
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Pre-execute hook failed: {e}")
|
|
104
|
+
result.warnings.append(f"Pre-execute hook failed: {str(e)}")
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Built-in validation hooks
|
|
110
|
+
async def validate_agent_dependencies(profile_path: Path) -> ValidationResult:
|
|
111
|
+
"""Validate that agent dependencies are available."""
|
|
112
|
+
result = ValidationResult(is_valid=True)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
with open(profile_path, 'r') as f:
|
|
116
|
+
profile_data = yaml.safe_load(f)
|
|
117
|
+
|
|
118
|
+
# Check for circular dependencies
|
|
119
|
+
agents = profile_data.get('agents', [])
|
|
120
|
+
for agent in agents:
|
|
121
|
+
dependencies = agent.get('dependencies', [])
|
|
122
|
+
# Simple check - in real implementation would need graph analysis
|
|
123
|
+
if agent['name'] in dependencies:
|
|
124
|
+
result.errors.append(f"Agent '{agent['name']}' has circular dependency")
|
|
125
|
+
result.is_valid = False
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
result.warnings.append(f"Could not check dependencies: {e}")
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def validate_security_constraints(agent_name: str, task: str) -> ValidationResult:
|
|
134
|
+
"""Validate security constraints before execution."""
|
|
135
|
+
result = ValidationResult(is_valid=True)
|
|
136
|
+
|
|
137
|
+
# Check for potential security issues in task
|
|
138
|
+
dangerous_patterns = [
|
|
139
|
+
'rm -rf /',
|
|
140
|
+
'sudo rm',
|
|
141
|
+
'format c:',
|
|
142
|
+
'__import__',
|
|
143
|
+
'eval(',
|
|
144
|
+
'exec(',
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
task_lower = task.lower()
|
|
148
|
+
for pattern in dangerous_patterns:
|
|
149
|
+
if pattern in task_lower:
|
|
150
|
+
result.errors.append(f"Potentially dangerous pattern detected: {pattern}")
|
|
151
|
+
result.is_valid = False
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ValidationError(Exception):
|
|
157
|
+
"""Raised when validation fails."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, message: str, validation_result: Optional[ValidationResult] = None):
|
|
160
|
+
"""Initialize validation error."""
|
|
161
|
+
super().__init__(message)
|
|
162
|
+
self.validation_result = validation_result
|
|
163
|
+
|
|
164
|
+
def get_detailed_message(self) -> str:
|
|
165
|
+
"""Get detailed error message including validation results."""
|
|
166
|
+
if not self.validation_result:
|
|
167
|
+
return str(self)
|
|
168
|
+
|
|
169
|
+
lines = [str(self)]
|
|
170
|
+
|
|
171
|
+
if self.validation_result.errors:
|
|
172
|
+
lines.append("\nErrors:")
|
|
173
|
+
for error in self.validation_result.errors:
|
|
174
|
+
lines.append(f" - {error}")
|
|
175
|
+
|
|
176
|
+
if self.validation_result.warnings:
|
|
177
|
+
lines.append("\nWarnings:")
|
|
178
|
+
for warning in self.validation_result.warnings:
|
|
179
|
+
lines.append(f" - {warning}")
|
|
180
|
+
|
|
181
|
+
return '\n'.join(lines)
|