claude-mpm 4.1.8__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/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/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 +77 -28
- claude_mpm/cli/commands/configure_tui.py +60 -60
- 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/debug_parser.py +319 -0
- 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 +386 -113
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- 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 +1 -2
- 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 +4 -4
- claude_mpm/services/socketio/server/core.py +100 -11
- claude_mpm/services/socketio/server/main.py +8 -2
- 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.8.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +102 -73
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
|
@@ -7,10 +7,12 @@ claude-mpm hooks in the Claude Code environment.
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import shutil
|
|
11
12
|
import stat
|
|
13
|
+
import subprocess
|
|
12
14
|
from pathlib import Path
|
|
13
|
-
from typing import Dict, List, Tuple
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
16
|
|
|
15
17
|
from ...core.logger import get_logger
|
|
16
18
|
|
|
@@ -18,11 +20,11 @@ from ...core.logger import get_logger
|
|
|
18
20
|
class HookInstaller:
|
|
19
21
|
"""Manages installation and configuration of Claude MPM hooks."""
|
|
20
22
|
|
|
21
|
-
#
|
|
23
|
+
# Note: SMART_HOOK_SCRIPT is deprecated - we now use deployment-root script
|
|
24
|
+
# Keep for backward compatibility during transition
|
|
22
25
|
SMART_HOOK_SCRIPT = """#!/bin/bash
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# Works with pip installations, local development, and virtual environments
|
|
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
|
|
26
28
|
|
|
27
29
|
# Function to find claude-mpm installation
|
|
28
30
|
find_claude_mpm() {
|
|
@@ -41,7 +43,7 @@ find_claude_mpm() {
|
|
|
41
43
|
return 0
|
|
42
44
|
fi
|
|
43
45
|
fi
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
# Method 2: Check common development locations
|
|
46
48
|
local dev_locations=(
|
|
47
49
|
"$HOME/Projects/claude-mpm"
|
|
@@ -55,14 +57,14 @@ find_claude_mpm() {
|
|
|
55
57
|
"$(pwd)/claude-mpm"
|
|
56
58
|
"$(pwd)"
|
|
57
59
|
)
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
for loc in "${dev_locations[@]}"; do
|
|
60
62
|
if [ -f "$loc/src/claude_mpm/__init__.py" ]; then
|
|
61
63
|
echo "$loc"
|
|
62
64
|
return 0
|
|
63
65
|
fi
|
|
64
66
|
done
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
# Method 3: Try to find via Python import
|
|
67
69
|
local python_path=$(python3 -c "
|
|
68
70
|
try:
|
|
@@ -86,12 +88,12 @@ try:
|
|
|
86
88
|
except:
|
|
87
89
|
pass
|
|
88
90
|
" 2>/dev/null)
|
|
89
|
-
|
|
91
|
+
|
|
90
92
|
if [ -n "$python_path" ]; then
|
|
91
93
|
echo "$python_path"
|
|
92
94
|
return 0
|
|
93
95
|
fi
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
# Method 4: Search in PATH for claude-mpm installations
|
|
96
98
|
local IFS=':'
|
|
97
99
|
for path_dir in $PATH; do
|
|
@@ -104,14 +106,14 @@ except:
|
|
|
104
106
|
fi
|
|
105
107
|
fi
|
|
106
108
|
done
|
|
107
|
-
|
|
109
|
+
|
|
108
110
|
return 1
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
# Function to setup Python environment
|
|
112
114
|
setup_python_env() {
|
|
113
115
|
local project_dir="$1"
|
|
114
|
-
|
|
116
|
+
|
|
115
117
|
# Check for virtual environment in the project
|
|
116
118
|
if [ -f "$project_dir/venv/bin/activate" ]; then
|
|
117
119
|
source "$project_dir/venv/bin/activate"
|
|
@@ -127,7 +129,7 @@ setup_python_env() {
|
|
|
127
129
|
else
|
|
128
130
|
export PYTHON_CMD="python"
|
|
129
131
|
fi
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
# Set PYTHONPATH for development installs
|
|
132
134
|
if [ -d "$project_dir/src" ]; then
|
|
133
135
|
export PYTHONPATH="$project_dir/src:$PYTHONPATH"
|
|
@@ -140,10 +142,10 @@ main() {
|
|
|
140
142
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
141
143
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Smart hook starting..." >> /tmp/claude-mpm-hook.log
|
|
142
144
|
fi
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
# Find claude-mpm installation
|
|
145
147
|
PROJECT_DIR=$(find_claude_mpm)
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
if [ -z "$PROJECT_DIR" ]; then
|
|
148
150
|
# Claude MPM not found - return continue to not block Claude
|
|
149
151
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
@@ -152,23 +154,23 @@ main() {
|
|
|
152
154
|
echo '{"action": "continue"}'
|
|
153
155
|
exit 0
|
|
154
156
|
fi
|
|
155
|
-
|
|
157
|
+
|
|
156
158
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
157
159
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Found claude-mpm at: $PROJECT_DIR" >> /tmp/claude-mpm-hook.log
|
|
158
160
|
fi
|
|
159
|
-
|
|
161
|
+
|
|
160
162
|
# Setup Python environment
|
|
161
163
|
setup_python_env "$PROJECT_DIR"
|
|
162
|
-
|
|
164
|
+
|
|
163
165
|
# Debug logging
|
|
164
166
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
165
167
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHON_CMD: $PYTHON_CMD" >> /tmp/claude-mpm-hook.log
|
|
166
168
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/claude-mpm-hook.log
|
|
167
169
|
fi
|
|
168
|
-
|
|
170
|
+
|
|
169
171
|
# Set Socket.IO configuration for hook events
|
|
170
172
|
export CLAUDE_MPM_SOCKETIO_PORT="${CLAUDE_MPM_SOCKETIO_PORT:-8765}"
|
|
171
|
-
|
|
173
|
+
|
|
172
174
|
# Run the hook handler
|
|
173
175
|
if ! "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log; then
|
|
174
176
|
# If the Python handler fails, always return continue to not block Claude
|
|
@@ -179,7 +181,7 @@ main() {
|
|
|
179
181
|
echo '{"action": "continue"}'
|
|
180
182
|
exit 0
|
|
181
183
|
fi
|
|
182
|
-
|
|
184
|
+
|
|
183
185
|
# Success
|
|
184
186
|
exit 0
|
|
185
187
|
}
|
|
@@ -188,12 +190,161 @@ main() {
|
|
|
188
190
|
main "$@"
|
|
189
191
|
"""
|
|
190
192
|
|
|
193
|
+
# Minimum Claude Code version required for hook monitoring
|
|
194
|
+
MIN_CLAUDE_VERSION = "1.0.92"
|
|
195
|
+
|
|
191
196
|
def __init__(self):
|
|
192
197
|
"""Initialize the hook installer."""
|
|
193
198
|
self.logger = get_logger(__name__)
|
|
194
199
|
self.claude_dir = Path.home() / ".claude"
|
|
195
|
-
self.hooks_dir = self.claude_dir / "hooks"
|
|
200
|
+
self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
|
|
201
|
+
# Use settings.json for hooks (Claude Code reads from this file)
|
|
196
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}")
|
|
197
348
|
|
|
198
349
|
def install_hooks(self, force: bool = False) -> bool:
|
|
199
350
|
"""
|
|
@@ -208,25 +359,44 @@ main "$@"
|
|
|
208
359
|
try:
|
|
209
360
|
self.logger.info("Starting hook installation...")
|
|
210
361
|
|
|
211
|
-
#
|
|
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)
|
|
212
379
|
self.claude_dir.mkdir(exist_ok=True)
|
|
213
|
-
self.hooks_dir.mkdir(exist_ok=True)
|
|
214
380
|
|
|
215
|
-
#
|
|
216
|
-
|
|
217
|
-
|
|
381
|
+
# Get the deployment-root hook script path
|
|
382
|
+
try:
|
|
383
|
+
hook_script_path = self.get_hook_script_path()
|
|
218
384
|
self.logger.info(
|
|
219
|
-
"
|
|
385
|
+
f"Using deployment-root hook script: {hook_script_path}"
|
|
220
386
|
)
|
|
221
|
-
|
|
222
|
-
self.
|
|
387
|
+
except FileNotFoundError as e:
|
|
388
|
+
self.logger.error(f"Failed to locate hook script: {e}")
|
|
389
|
+
return False
|
|
223
390
|
|
|
224
|
-
# Update Claude settings
|
|
391
|
+
# Update Claude settings to use deployment-root script
|
|
225
392
|
self._update_claude_settings(hook_script_path)
|
|
226
393
|
|
|
227
394
|
# Install commands if available
|
|
228
395
|
self._install_commands()
|
|
229
396
|
|
|
397
|
+
# Clean up old deployed scripts if they exist
|
|
398
|
+
self._cleanup_old_deployment()
|
|
399
|
+
|
|
230
400
|
self.logger.info("Hook installation completed successfully!")
|
|
231
401
|
return True
|
|
232
402
|
|
|
@@ -234,61 +404,100 @@ main "$@"
|
|
|
234
404
|
self.logger.error(f"Hook installation failed: {e}")
|
|
235
405
|
return False
|
|
236
406
|
|
|
237
|
-
def
|
|
238
|
-
"""
|
|
239
|
-
self.
|
|
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}")
|
|
240
416
|
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
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)
|
|
244
433
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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}")
|
|
248
438
|
|
|
249
|
-
|
|
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}")
|
|
250
446
|
|
|
251
447
|
def _update_claude_settings(self, hook_script_path: Path) -> None:
|
|
252
448
|
"""Update Claude settings to use the installed hook."""
|
|
253
449
|
self.logger.info("Updating Claude settings...")
|
|
254
450
|
|
|
255
|
-
# Load existing settings or create new
|
|
451
|
+
# Load existing settings.json or create new
|
|
256
452
|
if self.settings_file.exists():
|
|
257
453
|
with open(self.settings_file) as f:
|
|
258
454
|
settings = json.load(f)
|
|
259
|
-
self.logger.info("Found existing Claude settings")
|
|
455
|
+
self.logger.info(f"Found existing Claude settings at {self.settings_file}")
|
|
260
456
|
else:
|
|
261
457
|
settings = {}
|
|
262
|
-
self.logger.info("Creating new Claude settings")
|
|
458
|
+
self.logger.info(f"Creating new Claude settings at {self.settings_file}")
|
|
263
459
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
465
|
|
|
270
|
-
# Update
|
|
466
|
+
# Update hooks section
|
|
271
467
|
if "hooks" not in settings:
|
|
272
468
|
settings["hooks"] = {}
|
|
273
469
|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
287
493
|
with open(self.settings_file, "w") as f:
|
|
288
494
|
json.dump(settings, f, indent=2)
|
|
289
495
|
|
|
290
496
|
self.logger.info(f"Updated Claude settings at {self.settings_file}")
|
|
291
497
|
|
|
498
|
+
# Clean up hooks from old settings.json if present
|
|
499
|
+
self._cleanup_old_settings()
|
|
500
|
+
|
|
292
501
|
def _install_commands(self) -> None:
|
|
293
502
|
"""Install custom commands for Claude Code."""
|
|
294
503
|
# Find commands directory in the package
|
|
@@ -322,14 +531,23 @@ main "$@"
|
|
|
322
531
|
"""
|
|
323
532
|
issues = []
|
|
324
533
|
|
|
325
|
-
# Check
|
|
326
|
-
|
|
327
|
-
if not
|
|
328
|
-
issues.append(
|
|
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
|
|
329
540
|
|
|
330
|
-
# Check hook script
|
|
331
|
-
|
|
332
|
-
|
|
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))
|
|
333
551
|
|
|
334
552
|
# Check Claude settings
|
|
335
553
|
if not self.settings_file.exists():
|
|
@@ -343,7 +561,13 @@ main "$@"
|
|
|
343
561
|
issues.append("No hooks configured in Claude settings")
|
|
344
562
|
else:
|
|
345
563
|
# Check for required event types
|
|
346
|
-
required_events = [
|
|
564
|
+
required_events = [
|
|
565
|
+
"Stop",
|
|
566
|
+
"SubagentStop",
|
|
567
|
+
"SubagentStart",
|
|
568
|
+
"PreToolUse",
|
|
569
|
+
"PostToolUse",
|
|
570
|
+
]
|
|
347
571
|
for event in required_events:
|
|
348
572
|
if event not in settings["hooks"]:
|
|
349
573
|
issues.append(
|
|
@@ -372,47 +596,59 @@ main "$@"
|
|
|
372
596
|
try:
|
|
373
597
|
self.logger.info("Uninstalling hooks...")
|
|
374
598
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
if
|
|
378
|
-
|
|
379
|
-
self.logger.info(f"Removed
|
|
380
|
-
|
|
381
|
-
# Remove from Claude settings
|
|
382
|
-
if self.settings_file.exists():
|
|
383
|
-
with open(self.settings_file) as f:
|
|
384
|
-
settings = json.load(f)
|
|
385
|
-
|
|
386
|
-
if "hooks" in settings:
|
|
387
|
-
# Remove claude-mpm hooks
|
|
388
|
-
for event_type in list(settings["hooks"].keys()):
|
|
389
|
-
hooks = settings["hooks"][event_type]
|
|
390
|
-
# Filter out claude-mpm hooks
|
|
391
|
-
filtered_hooks = [
|
|
392
|
-
h
|
|
393
|
-
for h in hooks
|
|
394
|
-
if not (
|
|
395
|
-
isinstance(h, dict)
|
|
396
|
-
and h.get("hooks", [{}])[0]
|
|
397
|
-
.get("command", "")
|
|
398
|
-
.endswith("claude-mpm-hook.sh")
|
|
399
|
-
)
|
|
400
|
-
]
|
|
401
|
-
|
|
402
|
-
if filtered_hooks:
|
|
403
|
-
settings["hooks"][event_type] = filtered_hooks
|
|
404
|
-
else:
|
|
405
|
-
del settings["hooks"][event_type]
|
|
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}")
|
|
406
604
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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)
|
|
410
610
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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}")
|
|
416
652
|
|
|
417
653
|
self.logger.info("Hook uninstallation completed")
|
|
418
654
|
return True
|
|
@@ -428,28 +664,65 @@ main "$@"
|
|
|
428
664
|
Returns:
|
|
429
665
|
Dictionary with status information
|
|
430
666
|
"""
|
|
667
|
+
# Check version compatibility
|
|
668
|
+
claude_version = self.get_claude_version()
|
|
669
|
+
is_compatible, version_message = self.is_version_compatible()
|
|
670
|
+
|
|
431
671
|
is_valid, issues = self.verify_hooks()
|
|
432
672
|
|
|
433
|
-
|
|
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
|
|
434
681
|
|
|
435
682
|
status = {
|
|
436
|
-
"installed":
|
|
683
|
+
"installed": script_exists and self.settings_file.exists(),
|
|
437
684
|
"valid": is_valid,
|
|
438
685
|
"issues": issues,
|
|
439
|
-
"hook_script":
|
|
686
|
+
"hook_script": hook_script_str,
|
|
440
687
|
"settings_file": (
|
|
441
688
|
str(self.settings_file) if self.settings_file.exists() else None
|
|
442
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
|
|
443
694
|
}
|
|
444
695
|
|
|
445
696
|
# Check Claude settings for hook configuration
|
|
697
|
+
# Check both settings files to understand current state
|
|
698
|
+
configured_in_local = False
|
|
699
|
+
|
|
446
700
|
if self.settings_file.exists():
|
|
447
701
|
try:
|
|
448
702
|
with open(self.settings_file) as f:
|
|
449
703
|
settings = json.load(f)
|
|
450
704
|
if "hooks" in settings:
|
|
451
705
|
status["configured_events"] = list(settings["hooks"].keys())
|
|
706
|
+
configured_in_local = True
|
|
452
707
|
except:
|
|
453
708
|
pass
|
|
454
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
|
+
|
|
455
728
|
return status
|