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.
Files changed (109) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/OUTPUT_STYLE.md +73 -0
  4. claude_mpm/agents/agents_metadata.py +57 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  6. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  7. claude_mpm/agents/templates/agent-manager.json +263 -17
  8. claude_mpm/agents/templates/agent-manager.md +248 -10
  9. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  10. claude_mpm/agents/templates/code_analyzer.json +18 -8
  11. claude_mpm/agents/templates/engineer.json +1 -1
  12. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  13. claude_mpm/agents/templates/qa.json +1 -1
  14. claude_mpm/agents/templates/research.json +1 -1
  15. claude_mpm/cli/__init__.py +4 -0
  16. claude_mpm/cli/commands/__init__.py +6 -0
  17. claude_mpm/cli/commands/analyze.py +547 -0
  18. claude_mpm/cli/commands/analyze_code.py +524 -0
  19. claude_mpm/cli/commands/configure.py +223 -25
  20. claude_mpm/cli/commands/configure_tui.py +65 -61
  21. claude_mpm/cli/commands/debug.py +1387 -0
  22. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  23. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  24. claude_mpm/cli/parsers/base_parser.py +29 -0
  25. claude_mpm/cli/parsers/configure_parser.py +23 -0
  26. claude_mpm/cli/parsers/debug_parser.py +319 -0
  27. claude_mpm/config/socketio_config.py +21 -21
  28. claude_mpm/constants.py +3 -1
  29. claude_mpm/core/framework_loader.py +148 -6
  30. claude_mpm/core/log_manager.py +16 -13
  31. claude_mpm/core/logger.py +1 -1
  32. claude_mpm/core/unified_agent_registry.py +1 -1
  33. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  34. claude_mpm/dashboard/analysis_runner.py +428 -0
  35. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  36. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  37. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  38. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  39. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  40. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  41. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  42. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  43. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  44. claude_mpm/dashboard/static/css/activity.css +549 -0
  45. claude_mpm/dashboard/static/css/code-tree.css +846 -0
  46. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  47. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  48. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  49. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  50. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  51. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  52. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  53. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  54. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  55. claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
  56. claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
  57. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  58. claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
  59. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  60. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  61. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  62. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  63. claude_mpm/dashboard/static/js/dashboard.js +39 -0
  64. claude_mpm/dashboard/static/js/socket-client.js +414 -20
  65. claude_mpm/dashboard/templates/index.html +184 -4
  66. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  67. claude_mpm/hooks/claude_hooks/installer.py +728 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  69. claude_mpm/scripts/socketio_daemon.py +121 -8
  70. claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
  71. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  72. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  73. claude_mpm/services/agents/memory/memory_format_service.py +1 -5
  74. claude_mpm/services/cli/agent_cleanup_service.py +1 -2
  75. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  76. claude_mpm/services/cli/agent_validation_service.py +3 -4
  77. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  78. claude_mpm/services/cli/startup_checker.py +0 -10
  79. claude_mpm/services/core/cache_manager.py +1 -2
  80. claude_mpm/services/core/path_resolver.py +1 -4
  81. claude_mpm/services/core/service_container.py +2 -2
  82. claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
  83. claude_mpm/services/event_bus/direct_relay.py +98 -20
  84. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  85. claude_mpm/services/infrastructure/monitoring.py +11 -11
  86. claude_mpm/services/project/architecture_analyzer.py +1 -1
  87. claude_mpm/services/project/dependency_analyzer.py +4 -4
  88. claude_mpm/services/project/language_analyzer.py +3 -3
  89. claude_mpm/services/project/metrics_collector.py +3 -6
  90. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  91. claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
  92. claude_mpm/services/socketio/handlers/registry.py +2 -0
  93. claude_mpm/services/socketio/server/connection_manager.py +95 -65
  94. claude_mpm/services/socketio/server/core.py +125 -17
  95. claude_mpm/services/socketio/server/main.py +44 -5
  96. claude_mpm/services/visualization/__init__.py +19 -0
  97. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  98. claude_mpm/tools/__main__.py +208 -0
  99. claude_mpm/tools/code_tree_analyzer.py +778 -0
  100. claude_mpm/tools/code_tree_builder.py +632 -0
  101. claude_mpm/tools/code_tree_events.py +318 -0
  102. claude_mpm/tools/socketio_debug.py +671 -0
  103. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
  104. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
  105. claude_mpm/agents/schema/agent_schema.json +0 -314
  106. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
  107. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
  108. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
  109. {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