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.
Files changed (103) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/agents_metadata.py +57 -0
  4. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  6. claude_mpm/agents/templates/agent-manager.json +263 -17
  7. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  8. claude_mpm/agents/templates/code_analyzer.json +18 -8
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/cli/__init__.py +4 -0
  14. claude_mpm/cli/commands/__init__.py +6 -0
  15. claude_mpm/cli/commands/analyze.py +547 -0
  16. claude_mpm/cli/commands/analyze_code.py +524 -0
  17. claude_mpm/cli/commands/configure.py +77 -28
  18. claude_mpm/cli/commands/configure_tui.py +60 -60
  19. claude_mpm/cli/commands/debug.py +1387 -0
  20. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  21. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  22. claude_mpm/cli/parsers/base_parser.py +29 -0
  23. claude_mpm/cli/parsers/debug_parser.py +319 -0
  24. claude_mpm/constants.py +3 -1
  25. claude_mpm/core/framework_loader.py +148 -6
  26. claude_mpm/core/log_manager.py +16 -13
  27. claude_mpm/core/logger.py +1 -1
  28. claude_mpm/core/unified_agent_registry.py +1 -1
  29. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  30. claude_mpm/dashboard/analysis_runner.py +428 -0
  31. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  32. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  33. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  34. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  35. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  36. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  37. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  38. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  39. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  40. claude_mpm/dashboard/static/css/activity.css +549 -0
  41. claude_mpm/dashboard/static/css/code-tree.css +846 -0
  42. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  43. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  44. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  45. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  46. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  47. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  48. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  49. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  50. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  51. claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
  52. claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
  53. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  54. claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
  55. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  56. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  57. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  58. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  59. claude_mpm/dashboard/static/js/dashboard.js +39 -0
  60. claude_mpm/dashboard/static/js/socket-client.js +414 -20
  61. claude_mpm/dashboard/templates/index.html +184 -4
  62. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  63. claude_mpm/hooks/claude_hooks/installer.py +386 -113
  64. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  65. claude_mpm/scripts/socketio_daemon.py +121 -8
  66. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  67. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  68. claude_mpm/services/agents/memory/memory_format_service.py +1 -5
  69. claude_mpm/services/cli/agent_cleanup_service.py +1 -2
  70. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  71. claude_mpm/services/cli/agent_validation_service.py +3 -4
  72. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  73. claude_mpm/services/cli/startup_checker.py +0 -10
  74. claude_mpm/services/core/cache_manager.py +1 -2
  75. claude_mpm/services/core/path_resolver.py +1 -4
  76. claude_mpm/services/core/service_container.py +2 -2
  77. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  78. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  79. claude_mpm/services/infrastructure/monitoring.py +11 -11
  80. claude_mpm/services/project/architecture_analyzer.py +1 -1
  81. claude_mpm/services/project/dependency_analyzer.py +4 -4
  82. claude_mpm/services/project/language_analyzer.py +3 -3
  83. claude_mpm/services/project/metrics_collector.py +3 -6
  84. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  85. claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
  86. claude_mpm/services/socketio/handlers/registry.py +2 -0
  87. claude_mpm/services/socketio/server/connection_manager.py +4 -4
  88. claude_mpm/services/socketio/server/core.py +100 -11
  89. claude_mpm/services/socketio/server/main.py +8 -2
  90. claude_mpm/services/visualization/__init__.py +19 -0
  91. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  92. claude_mpm/tools/__main__.py +208 -0
  93. claude_mpm/tools/code_tree_analyzer.py +778 -0
  94. claude_mpm/tools/code_tree_builder.py +632 -0
  95. claude_mpm/tools/code_tree_events.py +318 -0
  96. claude_mpm/tools/socketio_debug.py +671 -0
  97. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
  98. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +102 -73
  99. claude_mpm/agents/schema/agent_schema.json +0 -314
  100. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
  101. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
  102. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
  103. {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
- # Smart hook script template
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
- # Claude MPM Smart Hook Handler
24
- # This script dynamically finds and routes hook events to claude-mpm
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
- # Create directories
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
- # Install smart hook script
216
- hook_script_path = self.hooks_dir / "claude-mpm-hook.sh"
217
- if hook_script_path.exists() and not force:
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
- "Hook script already exists. Use --force to overwrite."
385
+ f"Using deployment-root hook script: {hook_script_path}"
220
386
  )
221
- else:
222
- self._install_smart_hook_script(hook_script_path)
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 _install_smart_hook_script(self, hook_script_path: Path) -> None:
238
- """Install the smart hook script that dynamically finds claude-mpm."""
239
- self.logger.info(f"Installing smart hook script to {hook_script_path}")
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
- # Write the smart hook script
242
- with open(hook_script_path, "w") as f:
243
- f.write(self.SMART_HOOK_SCRIPT)
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
- # Make it executable
246
- st = os.stat(hook_script_path)
247
- os.chmod(hook_script_path, st.st_mode | stat.S_IEXEC)
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
- self.logger.info("Smart hook script installed and made executable")
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
- # Configure hooks
265
- hook_config = {
266
- "matcher": "*",
267
- "hooks": [{"type": "command", "command": str(hook_script_path.absolute())}],
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 settings
466
+ # Update hooks section
271
467
  if "hooks" not in settings:
272
468
  settings["hooks"] = {}
273
469
 
274
- # Add hooks for all event types
275
- event_types = [
276
- "UserPromptSubmit",
277
- "PreToolUse",
278
- "PostToolUse",
279
- "Stop",
280
- "SubagentStop",
281
- ]
282
-
283
- for event_type in event_types:
284
- settings["hooks"][event_type] = [hook_config]
285
-
286
- # Write settings
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 hook script exists
326
- hook_script_path = self.hooks_dir / "claude-mpm-hook.sh"
327
- if not hook_script_path.exists():
328
- issues.append(f"Hook script not found at {hook_script_path}")
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 is executable
331
- elif not os.access(hook_script_path, os.X_OK):
332
- issues.append(f"Hook script is not executable: {hook_script_path}")
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 = ["Stop", "SubagentStop"]
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
- # Remove hook script
376
- hook_script_path = self.hooks_dir / "claude-mpm-hook.sh"
377
- if hook_script_path.exists():
378
- hook_script_path.unlink()
379
- self.logger.info(f"Removed hook script: {hook_script_path}")
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
- # Clean up empty hooks section
408
- if not settings["hooks"]:
409
- del settings["hooks"]
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
- # Write back settings
412
- with open(self.settings_file, "w") as f:
413
- json.dump(settings, f, indent=2)
414
-
415
- self.logger.info("Removed hooks from Claude settings")
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
- hook_script_path = self.hooks_dir / "claude-mpm-hook.sh"
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": hook_script_path.exists(),
683
+ "installed": script_exists and self.settings_file.exists(),
437
684
  "valid": is_valid,
438
685
  "issues": issues,
439
- "hook_script": str(hook_script_path) if hook_script_path.exists() else None,
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