claude-mpm 4.1.7__py3-none-any.whl → 4.1.10__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 +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/OUTPUT_STYLE.md +73 -0
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agent-manager.md +248 -10
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +4 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +547 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +223 -25
- claude_mpm/cli/commands/configure_tui.py +65 -61
- claude_mpm/cli/commands/debug.py +1387 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +29 -0
- claude_mpm/cli/parsers/configure_parser.py +23 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/config/socketio_config.py +21 -21
- claude_mpm/constants.py +3 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +428 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +846 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +39 -0
- claude_mpm/dashboard/static/js/socket-client.js +414 -20
- claude_mpm/dashboard/templates/index.html +184 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +728 -0
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -5
- claude_mpm/services/cli/agent_cleanup_service.py +1 -2
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -10
- claude_mpm/services/core/cache_manager.py +1 -2
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
- claude_mpm/services/event_bus/direct_relay.py +98 -20
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +95 -65
- claude_mpm/services/socketio/server/core.py +125 -17
- claude_mpm/services/socketio/server/main.py +44 -5
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +778 -0
- claude_mpm/tools/code_tree_builder.py +632 -0
- claude_mpm/tools/code_tree_events.py +318 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook installer for Claude MPM integration with Claude Code.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to install, update, and manage
|
|
5
|
+
claude-mpm hooks in the Claude Code environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import stat
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from ...core.logger import get_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HookInstaller:
|
|
21
|
+
"""Manages installation and configuration of Claude MPM hooks."""
|
|
22
|
+
|
|
23
|
+
# Note: SMART_HOOK_SCRIPT is deprecated - we now use deployment-root script
|
|
24
|
+
# Keep for backward compatibility during transition
|
|
25
|
+
SMART_HOOK_SCRIPT = """#!/bin/bash
|
|
26
|
+
# DEPRECATED: This script is no longer used
|
|
27
|
+
# Claude MPM now uses deployment-root script at src/claude_mpm/scripts/claude-hook-handler.sh
|
|
28
|
+
|
|
29
|
+
# Function to find claude-mpm installation
|
|
30
|
+
find_claude_mpm() {
|
|
31
|
+
# Method 1: Check if claude-mpm is installed via pip
|
|
32
|
+
if command -v claude-mpm &> /dev/null; then
|
|
33
|
+
# Get the actual path of the claude-mpm command
|
|
34
|
+
local cmd_path=$(command -v claude-mpm)
|
|
35
|
+
if [ -L "$cmd_path" ]; then
|
|
36
|
+
# Follow symlink
|
|
37
|
+
cmd_path=$(readlink -f "$cmd_path")
|
|
38
|
+
fi
|
|
39
|
+
# Extract the base directory (usually site-packages or venv)
|
|
40
|
+
local base_dir=$(python3 -c "import claude_mpm; import os; print(os.path.dirname(os.path.dirname(claude_mpm.__file__)))" 2>/dev/null)
|
|
41
|
+
if [ -n "$base_dir" ]; then
|
|
42
|
+
echo "$base_dir"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Method 2: Check common development locations
|
|
48
|
+
local dev_locations=(
|
|
49
|
+
"$HOME/Projects/claude-mpm"
|
|
50
|
+
"$HOME/projects/claude-mpm"
|
|
51
|
+
"$HOME/dev/claude-mpm"
|
|
52
|
+
"$HOME/Development/claude-mpm"
|
|
53
|
+
"$HOME/src/claude-mpm"
|
|
54
|
+
"$HOME/code/claude-mpm"
|
|
55
|
+
"$HOME/workspace/claude-mpm"
|
|
56
|
+
"$HOME/claude-mpm"
|
|
57
|
+
"$(pwd)/claude-mpm"
|
|
58
|
+
"$(pwd)"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for loc in "${dev_locations[@]}"; do
|
|
62
|
+
if [ -f "$loc/src/claude_mpm/__init__.py" ]; then
|
|
63
|
+
echo "$loc"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
# Method 3: Try to find via Python import
|
|
69
|
+
local python_path=$(python3 -c "
|
|
70
|
+
try:
|
|
71
|
+
import claude_mpm
|
|
72
|
+
import os
|
|
73
|
+
# Get the package directory
|
|
74
|
+
pkg_dir = os.path.dirname(claude_mpm.__file__)
|
|
75
|
+
# Check if we're in a development install (src directory)
|
|
76
|
+
if 'src' in pkg_dir:
|
|
77
|
+
# Go up to find the project root
|
|
78
|
+
parts = pkg_dir.split(os.sep)
|
|
79
|
+
if 'src' in parts:
|
|
80
|
+
src_idx = parts.index('src')
|
|
81
|
+
project_root = os.sep.join(parts[:src_idx])
|
|
82
|
+
print(project_root)
|
|
83
|
+
else:
|
|
84
|
+
print(os.path.dirname(os.path.dirname(pkg_dir)))
|
|
85
|
+
else:
|
|
86
|
+
# Installed package - just return the package location
|
|
87
|
+
print(os.path.dirname(pkg_dir))
|
|
88
|
+
except:
|
|
89
|
+
pass
|
|
90
|
+
" 2>/dev/null)
|
|
91
|
+
|
|
92
|
+
if [ -n "$python_path" ]; then
|
|
93
|
+
echo "$python_path"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Method 4: Search in PATH for claude-mpm installations
|
|
98
|
+
local IFS=':'
|
|
99
|
+
for path_dir in $PATH; do
|
|
100
|
+
if [ -f "$path_dir/claude-mpm" ]; then
|
|
101
|
+
# Found claude-mpm executable, try to find its package
|
|
102
|
+
local pkg_dir=$(cd "$path_dir" && python3 -c "import claude_mpm; import os; print(os.path.dirname(os.path.dirname(claude_mpm.__file__)))" 2>/dev/null)
|
|
103
|
+
if [ -n "$pkg_dir" ]; then
|
|
104
|
+
echo "$pkg_dir"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Function to setup Python environment
|
|
114
|
+
setup_python_env() {
|
|
115
|
+
local project_dir="$1"
|
|
116
|
+
|
|
117
|
+
# Check for virtual environment in the project
|
|
118
|
+
if [ -f "$project_dir/venv/bin/activate" ]; then
|
|
119
|
+
source "$project_dir/venv/bin/activate"
|
|
120
|
+
export PYTHON_CMD="$project_dir/venv/bin/python"
|
|
121
|
+
elif [ -f "$project_dir/.venv/bin/activate" ]; then
|
|
122
|
+
source "$project_dir/.venv/bin/activate"
|
|
123
|
+
export PYTHON_CMD="$project_dir/.venv/bin/python"
|
|
124
|
+
elif [ -n "$VIRTUAL_ENV" ]; then
|
|
125
|
+
# Already in a virtual environment
|
|
126
|
+
export PYTHON_CMD="$VIRTUAL_ENV/bin/python"
|
|
127
|
+
elif command -v python3 &> /dev/null; then
|
|
128
|
+
export PYTHON_CMD="python3"
|
|
129
|
+
else
|
|
130
|
+
export PYTHON_CMD="python"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Set PYTHONPATH for development installs
|
|
134
|
+
if [ -d "$project_dir/src" ]; then
|
|
135
|
+
export PYTHONPATH="$project_dir/src:$PYTHONPATH"
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Main execution
|
|
140
|
+
main() {
|
|
141
|
+
# Debug mode (can be disabled in production)
|
|
142
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
143
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Smart hook starting..." >> /tmp/claude-mpm-hook.log
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Find claude-mpm installation
|
|
147
|
+
PROJECT_DIR=$(find_claude_mpm)
|
|
148
|
+
|
|
149
|
+
if [ -z "$PROJECT_DIR" ]; then
|
|
150
|
+
# Claude MPM not found - return continue to not block Claude
|
|
151
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
152
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Claude MPM not found, continuing..." >> /tmp/claude-mpm-hook.log
|
|
153
|
+
fi
|
|
154
|
+
echo '{"action": "continue"}'
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
159
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Found claude-mpm at: $PROJECT_DIR" >> /tmp/claude-mpm-hook.log
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Setup Python environment
|
|
163
|
+
setup_python_env "$PROJECT_DIR"
|
|
164
|
+
|
|
165
|
+
# Debug logging
|
|
166
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
167
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHON_CMD: $PYTHON_CMD" >> /tmp/claude-mpm-hook.log
|
|
168
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/claude-mpm-hook.log
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# Set Socket.IO configuration for hook events
|
|
172
|
+
export CLAUDE_MPM_SOCKETIO_PORT="${CLAUDE_MPM_SOCKETIO_PORT:-8765}"
|
|
173
|
+
|
|
174
|
+
# Run the hook handler
|
|
175
|
+
if ! "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log; then
|
|
176
|
+
# If the Python handler fails, always return continue to not block Claude
|
|
177
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
178
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/claude-mpm-hook-error.log" >> /tmp/claude-mpm-hook.log
|
|
179
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Error: $(cat /tmp/claude-mpm-hook-error.log 2>/dev/null | head -5)" >> /tmp/claude-mpm-hook.log
|
|
180
|
+
fi
|
|
181
|
+
echo '{"action": "continue"}'
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Success
|
|
186
|
+
exit 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Run main function
|
|
190
|
+
main "$@"
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
# Minimum Claude Code version required for hook monitoring
|
|
194
|
+
MIN_CLAUDE_VERSION = "1.0.92"
|
|
195
|
+
|
|
196
|
+
def __init__(self):
|
|
197
|
+
"""Initialize the hook installer."""
|
|
198
|
+
self.logger = get_logger(__name__)
|
|
199
|
+
self.claude_dir = Path.home() / ".claude"
|
|
200
|
+
self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
|
|
201
|
+
# Use settings.json for hooks (Claude Code reads from this file)
|
|
202
|
+
self.settings_file = self.claude_dir / "settings.json"
|
|
203
|
+
# Keep reference to old file for migration
|
|
204
|
+
self.old_settings_file = self.claude_dir / "settings.json"
|
|
205
|
+
self._claude_version: Optional[str] = None
|
|
206
|
+
self._hook_script_path: Optional[Path] = None
|
|
207
|
+
|
|
208
|
+
def get_claude_version(self) -> Optional[str]:
|
|
209
|
+
"""
|
|
210
|
+
Get the installed Claude Code version.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Version string (e.g., "1.0.92") or None if not detected
|
|
214
|
+
"""
|
|
215
|
+
if self._claude_version is not None:
|
|
216
|
+
return self._claude_version
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Run claude --version command
|
|
220
|
+
result = subprocess.run(
|
|
221
|
+
["claude", "--version"],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
timeout=5,
|
|
225
|
+
check=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if result.returncode == 0:
|
|
229
|
+
# Parse version from output (e.g., "1.0.92 (Claude Code)")
|
|
230
|
+
version_text = result.stdout.strip()
|
|
231
|
+
# Extract version number using regex
|
|
232
|
+
match = re.match(r"^([\d\.]+)", version_text)
|
|
233
|
+
if match:
|
|
234
|
+
self._claude_version = match.group(1)
|
|
235
|
+
self.logger.info(
|
|
236
|
+
f"Detected Claude Code version: {self._claude_version}"
|
|
237
|
+
)
|
|
238
|
+
return self._claude_version
|
|
239
|
+
else:
|
|
240
|
+
self.logger.warning(f"Failed to get Claude version: {result.stderr}")
|
|
241
|
+
|
|
242
|
+
except FileNotFoundError:
|
|
243
|
+
self.logger.warning("Claude Code command not found in PATH")
|
|
244
|
+
except subprocess.TimeoutExpired:
|
|
245
|
+
self.logger.warning("Claude version check timed out")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.warning(f"Error detecting Claude version: {e}")
|
|
248
|
+
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def is_version_compatible(self) -> Tuple[bool, str]:
|
|
252
|
+
"""
|
|
253
|
+
Check if the installed Claude Code version meets minimum requirements.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Tuple of (is_compatible, message)
|
|
257
|
+
"""
|
|
258
|
+
version = self.get_claude_version()
|
|
259
|
+
|
|
260
|
+
if version is None:
|
|
261
|
+
return (
|
|
262
|
+
False,
|
|
263
|
+
"Could not detect Claude Code version. Hooks require Claude Code to be installed.",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Parse version numbers for comparison
|
|
267
|
+
def parse_version(v: str) -> List[int]:
|
|
268
|
+
"""Parse semantic version string to list of integers."""
|
|
269
|
+
try:
|
|
270
|
+
return [int(x) for x in v.split(".")]
|
|
271
|
+
except (ValueError, AttributeError):
|
|
272
|
+
return [0]
|
|
273
|
+
|
|
274
|
+
current = parse_version(version)
|
|
275
|
+
required = parse_version(self.MIN_CLAUDE_VERSION)
|
|
276
|
+
|
|
277
|
+
# Compare versions (semantic versioning)
|
|
278
|
+
for i in range(max(len(current), len(required))):
|
|
279
|
+
curr_part = current[i] if i < len(current) else 0
|
|
280
|
+
req_part = required[i] if i < len(required) else 0
|
|
281
|
+
|
|
282
|
+
if curr_part < req_part:
|
|
283
|
+
return (
|
|
284
|
+
False,
|
|
285
|
+
f"Claude Code {version} does not support matcher-based hooks. "
|
|
286
|
+
f"Version {self.MIN_CLAUDE_VERSION} or higher is required for hook monitoring. "
|
|
287
|
+
f"Please upgrade Claude Code to enable dashboard monitoring features.",
|
|
288
|
+
)
|
|
289
|
+
if curr_part > req_part:
|
|
290
|
+
# Current version is higher, compatible
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
return (True, f"Claude Code {version} is compatible with hook monitoring.")
|
|
294
|
+
|
|
295
|
+
def get_hook_script_path(self) -> Path:
|
|
296
|
+
"""Get the path to the hook handler script based on installation method.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Path to the claude-hook-handler.sh script
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
FileNotFoundError: If the script cannot be found
|
|
303
|
+
"""
|
|
304
|
+
if self._hook_script_path and self._hook_script_path.exists():
|
|
305
|
+
return self._hook_script_path
|
|
306
|
+
|
|
307
|
+
import claude_mpm
|
|
308
|
+
|
|
309
|
+
# Get the claude_mpm package directory
|
|
310
|
+
package_dir = Path(claude_mpm.__file__).parent
|
|
311
|
+
|
|
312
|
+
# Check if we're in a development environment (src structure)
|
|
313
|
+
if "src/claude_mpm" in str(package_dir):
|
|
314
|
+
# Development install - script is in src/claude_mpm/scripts
|
|
315
|
+
script_path = package_dir / "scripts" / "claude-hook-handler.sh"
|
|
316
|
+
else:
|
|
317
|
+
# Pip install - script should be in package/scripts
|
|
318
|
+
script_path = package_dir / "scripts" / "claude-hook-handler.sh"
|
|
319
|
+
|
|
320
|
+
# Verify the script exists
|
|
321
|
+
if not script_path.exists():
|
|
322
|
+
# Try alternative location for editable installs
|
|
323
|
+
project_root = package_dir.parent.parent
|
|
324
|
+
alt_path = (
|
|
325
|
+
project_root
|
|
326
|
+
/ "src"
|
|
327
|
+
/ "claude_mpm"
|
|
328
|
+
/ "scripts"
|
|
329
|
+
/ "claude-hook-handler.sh"
|
|
330
|
+
)
|
|
331
|
+
if alt_path.exists():
|
|
332
|
+
script_path = alt_path
|
|
333
|
+
else:
|
|
334
|
+
raise FileNotFoundError(
|
|
335
|
+
f"Hook handler script not found. Searched:\n"
|
|
336
|
+
f" - {script_path}\n"
|
|
337
|
+
f" - {alt_path}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Make sure it's executable
|
|
341
|
+
if script_path.exists():
|
|
342
|
+
st = os.stat(script_path)
|
|
343
|
+
os.chmod(script_path, st.st_mode | stat.S_IEXEC)
|
|
344
|
+
self._hook_script_path = script_path
|
|
345
|
+
return script_path
|
|
346
|
+
|
|
347
|
+
raise FileNotFoundError(f"Hook handler script not found at {script_path}")
|
|
348
|
+
|
|
349
|
+
def install_hooks(self, force: bool = False) -> bool:
|
|
350
|
+
"""
|
|
351
|
+
Install Claude MPM hooks.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
force: Force reinstallation even if hooks already exist
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
True if installation successful, False otherwise
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
self.logger.info("Starting hook installation...")
|
|
361
|
+
|
|
362
|
+
# Check Claude Code version compatibility
|
|
363
|
+
is_compatible, version_message = self.is_version_compatible()
|
|
364
|
+
self.logger.info(version_message)
|
|
365
|
+
|
|
366
|
+
if not is_compatible:
|
|
367
|
+
self.logger.warning(
|
|
368
|
+
"Claude Code version is incompatible with hook monitoring. "
|
|
369
|
+
"Skipping hook installation to avoid configuration errors."
|
|
370
|
+
)
|
|
371
|
+
print(f"\n[Warning] {version_message}")
|
|
372
|
+
print(
|
|
373
|
+
"Hook-based monitoring features will be disabled. "
|
|
374
|
+
"The dashboard and other features will still work without real-time monitoring."
|
|
375
|
+
)
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
# Create Claude directory (hooks_dir no longer needed)
|
|
379
|
+
self.claude_dir.mkdir(exist_ok=True)
|
|
380
|
+
|
|
381
|
+
# Get the deployment-root hook script path
|
|
382
|
+
try:
|
|
383
|
+
hook_script_path = self.get_hook_script_path()
|
|
384
|
+
self.logger.info(
|
|
385
|
+
f"Using deployment-root hook script: {hook_script_path}"
|
|
386
|
+
)
|
|
387
|
+
except FileNotFoundError as e:
|
|
388
|
+
self.logger.error(f"Failed to locate hook script: {e}")
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
# Update Claude settings to use deployment-root script
|
|
392
|
+
self._update_claude_settings(hook_script_path)
|
|
393
|
+
|
|
394
|
+
# Install commands if available
|
|
395
|
+
self._install_commands()
|
|
396
|
+
|
|
397
|
+
# Clean up old deployed scripts if they exist
|
|
398
|
+
self._cleanup_old_deployment()
|
|
399
|
+
|
|
400
|
+
self.logger.info("Hook installation completed successfully!")
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
self.logger.error(f"Hook installation failed: {e}")
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
def _cleanup_old_deployment(self) -> None:
|
|
408
|
+
"""Clean up old deployed hook scripts if they exist."""
|
|
409
|
+
old_script = self.hooks_dir / "claude-mpm-hook.sh"
|
|
410
|
+
if old_script.exists():
|
|
411
|
+
try:
|
|
412
|
+
old_script.unlink()
|
|
413
|
+
self.logger.info(f"Removed old deployed script: {old_script}")
|
|
414
|
+
except Exception as e:
|
|
415
|
+
self.logger.warning(f"Could not remove old script {old_script}: {e}")
|
|
416
|
+
|
|
417
|
+
# Clean up hooks directory if empty
|
|
418
|
+
if self.hooks_dir.exists() and not any(self.hooks_dir.iterdir()):
|
|
419
|
+
try:
|
|
420
|
+
self.hooks_dir.rmdir()
|
|
421
|
+
self.logger.info(f"Removed empty hooks directory: {self.hooks_dir}")
|
|
422
|
+
except Exception as e:
|
|
423
|
+
self.logger.debug(f"Could not remove hooks directory: {e}")
|
|
424
|
+
|
|
425
|
+
def _cleanup_old_settings(self) -> None:
|
|
426
|
+
"""Remove hooks from old settings.json file if present."""
|
|
427
|
+
if not self.old_settings_file.exists():
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
with open(self.old_settings_file) as f:
|
|
432
|
+
old_settings = json.load(f)
|
|
433
|
+
|
|
434
|
+
# Remove hooks section if present
|
|
435
|
+
if "hooks" in old_settings:
|
|
436
|
+
del old_settings["hooks"]
|
|
437
|
+
self.logger.info(f"Removing hooks from {self.old_settings_file}")
|
|
438
|
+
|
|
439
|
+
# Write back the cleaned settings
|
|
440
|
+
with open(self.old_settings_file, "w") as f:
|
|
441
|
+
json.dump(old_settings, f, indent=2)
|
|
442
|
+
|
|
443
|
+
self.logger.info(f"Cleaned up hooks from {self.old_settings_file}")
|
|
444
|
+
except Exception as e:
|
|
445
|
+
self.logger.warning(f"Could not clean up old settings file: {e}")
|
|
446
|
+
|
|
447
|
+
def _update_claude_settings(self, hook_script_path: Path) -> None:
|
|
448
|
+
"""Update Claude settings to use the installed hook."""
|
|
449
|
+
self.logger.info("Updating Claude settings...")
|
|
450
|
+
|
|
451
|
+
# Load existing settings.json or create new
|
|
452
|
+
if self.settings_file.exists():
|
|
453
|
+
with open(self.settings_file) as f:
|
|
454
|
+
settings = json.load(f)
|
|
455
|
+
self.logger.info(f"Found existing Claude settings at {self.settings_file}")
|
|
456
|
+
else:
|
|
457
|
+
settings = {}
|
|
458
|
+
self.logger.info(f"Creating new Claude settings at {self.settings_file}")
|
|
459
|
+
|
|
460
|
+
# Preserve existing permissions and mcpServers if present
|
|
461
|
+
if "permissions" not in settings:
|
|
462
|
+
settings["permissions"] = {"allow": []}
|
|
463
|
+
if "enableAllProjectMcpServers" not in settings:
|
|
464
|
+
settings["enableAllProjectMcpServers"] = False
|
|
465
|
+
|
|
466
|
+
# Update hooks section
|
|
467
|
+
if "hooks" not in settings:
|
|
468
|
+
settings["hooks"] = {}
|
|
469
|
+
|
|
470
|
+
# Hook configuration for each event type
|
|
471
|
+
hook_command = {"type": "command", "command": str(hook_script_path.absolute())}
|
|
472
|
+
|
|
473
|
+
# Tool-related events need a matcher string
|
|
474
|
+
tool_events = ["PreToolUse", "PostToolUse"]
|
|
475
|
+
for event_type in tool_events:
|
|
476
|
+
settings["hooks"][event_type] = [
|
|
477
|
+
{
|
|
478
|
+
"matcher": "*", # String value to match all tools
|
|
479
|
+
"hooks": [hook_command],
|
|
480
|
+
}
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
# Non-tool events don't need a matcher
|
|
484
|
+
non_tool_events = ["UserPromptSubmit", "Stop", "SubagentStop", "SubagentStart"]
|
|
485
|
+
for event_type in non_tool_events:
|
|
486
|
+
settings["hooks"][event_type] = [
|
|
487
|
+
{
|
|
488
|
+
"hooks": [hook_command],
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
# Write settings to settings.json
|
|
493
|
+
with open(self.settings_file, "w") as f:
|
|
494
|
+
json.dump(settings, f, indent=2)
|
|
495
|
+
|
|
496
|
+
self.logger.info(f"Updated Claude settings at {self.settings_file}")
|
|
497
|
+
|
|
498
|
+
# Clean up hooks from old settings.json if present
|
|
499
|
+
self._cleanup_old_settings()
|
|
500
|
+
|
|
501
|
+
def _install_commands(self) -> None:
|
|
502
|
+
"""Install custom commands for Claude Code."""
|
|
503
|
+
# Find commands directory in the package
|
|
504
|
+
package_root = Path(__file__).parent.parent.parent.parent
|
|
505
|
+
commands_src = package_root / ".claude" / "commands"
|
|
506
|
+
|
|
507
|
+
if not commands_src.exists():
|
|
508
|
+
self.logger.debug(
|
|
509
|
+
"No commands directory found, skipping command installation"
|
|
510
|
+
)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
commands_dst = self.claude_dir / "commands"
|
|
514
|
+
commands_dst.mkdir(exist_ok=True)
|
|
515
|
+
|
|
516
|
+
for cmd_file in commands_src.glob("*.md"):
|
|
517
|
+
dst_file = commands_dst / cmd_file.name
|
|
518
|
+
shutil.copy2(cmd_file, dst_file)
|
|
519
|
+
self.logger.info(f"Installed command: {cmd_file.name}")
|
|
520
|
+
|
|
521
|
+
def update_hooks(self) -> bool:
|
|
522
|
+
"""Update existing hooks to the latest version."""
|
|
523
|
+
return self.install_hooks(force=True)
|
|
524
|
+
|
|
525
|
+
def verify_hooks(self) -> Tuple[bool, List[str]]:
|
|
526
|
+
"""
|
|
527
|
+
Verify that hooks are properly installed.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Tuple of (is_valid, list_of_issues)
|
|
531
|
+
"""
|
|
532
|
+
issues = []
|
|
533
|
+
|
|
534
|
+
# Check version compatibility first
|
|
535
|
+
is_compatible, version_message = self.is_version_compatible()
|
|
536
|
+
if not is_compatible:
|
|
537
|
+
issues.append(version_message)
|
|
538
|
+
# If version is incompatible, skip other checks as hooks shouldn't be installed
|
|
539
|
+
return False, issues
|
|
540
|
+
|
|
541
|
+
# Check hook script exists at deployment root
|
|
542
|
+
try:
|
|
543
|
+
hook_script_path = self.get_hook_script_path()
|
|
544
|
+
if not hook_script_path.exists():
|
|
545
|
+
issues.append(f"Hook script not found at {hook_script_path}")
|
|
546
|
+
# Check hook script is executable
|
|
547
|
+
elif not os.access(hook_script_path, os.X_OK):
|
|
548
|
+
issues.append(f"Hook script is not executable: {hook_script_path}")
|
|
549
|
+
except FileNotFoundError as e:
|
|
550
|
+
issues.append(str(e))
|
|
551
|
+
|
|
552
|
+
# Check Claude settings
|
|
553
|
+
if not self.settings_file.exists():
|
|
554
|
+
issues.append(f"Claude settings file not found at {self.settings_file}")
|
|
555
|
+
else:
|
|
556
|
+
try:
|
|
557
|
+
with open(self.settings_file) as f:
|
|
558
|
+
settings = json.load(f)
|
|
559
|
+
|
|
560
|
+
if "hooks" not in settings:
|
|
561
|
+
issues.append("No hooks configured in Claude settings")
|
|
562
|
+
else:
|
|
563
|
+
# Check for required event types
|
|
564
|
+
required_events = [
|
|
565
|
+
"Stop",
|
|
566
|
+
"SubagentStop",
|
|
567
|
+
"SubagentStart",
|
|
568
|
+
"PreToolUse",
|
|
569
|
+
"PostToolUse",
|
|
570
|
+
]
|
|
571
|
+
for event in required_events:
|
|
572
|
+
if event not in settings["hooks"]:
|
|
573
|
+
issues.append(
|
|
574
|
+
f"Missing hook configuration for {event} event"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
except json.JSONDecodeError as e:
|
|
578
|
+
issues.append(f"Invalid Claude settings JSON: {e}")
|
|
579
|
+
|
|
580
|
+
# Check if claude-mpm is accessible
|
|
581
|
+
try:
|
|
582
|
+
import claude_mpm
|
|
583
|
+
except ImportError:
|
|
584
|
+
issues.append("claude-mpm package not found in Python environment")
|
|
585
|
+
|
|
586
|
+
is_valid = len(issues) == 0
|
|
587
|
+
return is_valid, issues
|
|
588
|
+
|
|
589
|
+
def uninstall_hooks(self) -> bool:
|
|
590
|
+
"""
|
|
591
|
+
Remove Claude MPM hooks.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
True if uninstallation successful, False otherwise
|
|
595
|
+
"""
|
|
596
|
+
try:
|
|
597
|
+
self.logger.info("Uninstalling hooks...")
|
|
598
|
+
|
|
599
|
+
# Clean up old deployed scripts if they still exist
|
|
600
|
+
old_script = self.hooks_dir / "claude-mpm-hook.sh"
|
|
601
|
+
if old_script.exists():
|
|
602
|
+
old_script.unlink()
|
|
603
|
+
self.logger.info(f"Removed old deployed script: {old_script}")
|
|
604
|
+
|
|
605
|
+
# Remove from Claude settings (both old and new locations)
|
|
606
|
+
for settings_path in [self.settings_file, self.old_settings_file]:
|
|
607
|
+
if settings_path.exists():
|
|
608
|
+
with open(settings_path) as f:
|
|
609
|
+
settings = json.load(f)
|
|
610
|
+
|
|
611
|
+
if "hooks" in settings:
|
|
612
|
+
# Remove claude-mpm hooks
|
|
613
|
+
for event_type in list(settings["hooks"].keys()):
|
|
614
|
+
hooks = settings["hooks"][event_type]
|
|
615
|
+
# Filter out claude-mpm hooks
|
|
616
|
+
filtered_hooks = []
|
|
617
|
+
for h in hooks:
|
|
618
|
+
# Check if this is a claude-mpm hook
|
|
619
|
+
is_claude_mpm = False
|
|
620
|
+
if isinstance(h, dict) and "hooks" in h:
|
|
621
|
+
# Check each hook command in the hooks array
|
|
622
|
+
for hook_cmd in h.get("hooks", []):
|
|
623
|
+
if (
|
|
624
|
+
isinstance(hook_cmd, dict)
|
|
625
|
+
and hook_cmd.get("type") == "command"
|
|
626
|
+
):
|
|
627
|
+
cmd = hook_cmd.get("command", "")
|
|
628
|
+
if (
|
|
629
|
+
"claude-hook-handler.sh" in cmd
|
|
630
|
+
or cmd.endswith("claude-mpm-hook.sh")
|
|
631
|
+
):
|
|
632
|
+
is_claude_mpm = True
|
|
633
|
+
break
|
|
634
|
+
|
|
635
|
+
if not is_claude_mpm:
|
|
636
|
+
filtered_hooks.append(h)
|
|
637
|
+
|
|
638
|
+
if filtered_hooks:
|
|
639
|
+
settings["hooks"][event_type] = filtered_hooks
|
|
640
|
+
else:
|
|
641
|
+
del settings["hooks"][event_type]
|
|
642
|
+
|
|
643
|
+
# Clean up empty hooks section
|
|
644
|
+
if not settings["hooks"]:
|
|
645
|
+
del settings["hooks"]
|
|
646
|
+
|
|
647
|
+
# Write back settings
|
|
648
|
+
with open(settings_path, "w") as f:
|
|
649
|
+
json.dump(settings, f, indent=2)
|
|
650
|
+
|
|
651
|
+
self.logger.info(f"Removed hooks from {settings_path}")
|
|
652
|
+
|
|
653
|
+
self.logger.info("Hook uninstallation completed")
|
|
654
|
+
return True
|
|
655
|
+
|
|
656
|
+
except Exception as e:
|
|
657
|
+
self.logger.error(f"Hook uninstallation failed: {e}")
|
|
658
|
+
return False
|
|
659
|
+
|
|
660
|
+
def get_status(self) -> Dict[str, any]:
|
|
661
|
+
"""
|
|
662
|
+
Get the current status of hook installation.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Dictionary with status information
|
|
666
|
+
"""
|
|
667
|
+
# Check version compatibility
|
|
668
|
+
claude_version = self.get_claude_version()
|
|
669
|
+
is_compatible, version_message = self.is_version_compatible()
|
|
670
|
+
|
|
671
|
+
is_valid, issues = self.verify_hooks()
|
|
672
|
+
|
|
673
|
+
# Try to get deployment-root script path
|
|
674
|
+
try:
|
|
675
|
+
hook_script_path = self.get_hook_script_path()
|
|
676
|
+
hook_script_str = str(hook_script_path)
|
|
677
|
+
script_exists = hook_script_path.exists()
|
|
678
|
+
except FileNotFoundError:
|
|
679
|
+
hook_script_str = None
|
|
680
|
+
script_exists = False
|
|
681
|
+
|
|
682
|
+
status = {
|
|
683
|
+
"installed": script_exists and self.settings_file.exists(),
|
|
684
|
+
"valid": is_valid,
|
|
685
|
+
"issues": issues,
|
|
686
|
+
"hook_script": hook_script_str,
|
|
687
|
+
"settings_file": (
|
|
688
|
+
str(self.settings_file) if self.settings_file.exists() else None
|
|
689
|
+
),
|
|
690
|
+
"claude_version": claude_version,
|
|
691
|
+
"version_compatible": is_compatible,
|
|
692
|
+
"version_message": version_message,
|
|
693
|
+
"deployment_type": "deployment-root", # New field to indicate new architecture
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
# Check Claude settings for hook configuration
|
|
697
|
+
# Check both settings files to understand current state
|
|
698
|
+
configured_in_local = False
|
|
699
|
+
|
|
700
|
+
if self.settings_file.exists():
|
|
701
|
+
try:
|
|
702
|
+
with open(self.settings_file) as f:
|
|
703
|
+
settings = json.load(f)
|
|
704
|
+
if "hooks" in settings:
|
|
705
|
+
status["configured_events"] = list(settings["hooks"].keys())
|
|
706
|
+
configured_in_local = True
|
|
707
|
+
except:
|
|
708
|
+
pass
|
|
709
|
+
|
|
710
|
+
# Also check old settings file
|
|
711
|
+
if self.old_settings_file.exists():
|
|
712
|
+
try:
|
|
713
|
+
with open(self.old_settings_file) as f:
|
|
714
|
+
old_settings = json.load(f)
|
|
715
|
+
if "hooks" in old_settings:
|
|
716
|
+
status["old_file_has_hooks"] = True
|
|
717
|
+
if not configured_in_local:
|
|
718
|
+
status["warning"] = (
|
|
719
|
+
"Hooks found in settings.local.json but Claude Code reads from settings.json"
|
|
720
|
+
)
|
|
721
|
+
except:
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
status["settings_location"] = (
|
|
725
|
+
"settings.json" if configured_in_local else "not configured"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
return status
|