claude-mpm 5.6.23__py3-none-any.whl → 5.6.72__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (80) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/auth/__init__.py +35 -0
  3. claude_mpm/auth/callback_server.py +328 -0
  4. claude_mpm/auth/models.py +104 -0
  5. claude_mpm/auth/oauth_manager.py +266 -0
  6. claude_mpm/auth/providers/__init__.py +12 -0
  7. claude_mpm/auth/providers/base.py +165 -0
  8. claude_mpm/auth/providers/google.py +261 -0
  9. claude_mpm/auth/token_storage.py +252 -0
  10. claude_mpm/cli/commands/commander.py +6 -6
  11. claude_mpm/cli/commands/mcp.py +29 -17
  12. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  13. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  14. claude_mpm/cli/commands/oauth.py +481 -0
  15. claude_mpm/cli/executor.py +9 -0
  16. claude_mpm/cli/helpers.py +1 -1
  17. claude_mpm/cli/parsers/base_parser.py +13 -0
  18. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  19. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  20. claude_mpm/cli/startup.py +150 -33
  21. claude_mpm/cli/startup_display.py +3 -2
  22. claude_mpm/commander/chat/cli.py +5 -2
  23. claude_mpm/commander/chat/commands.py +42 -16
  24. claude_mpm/commander/chat/repl.py +1581 -70
  25. claude_mpm/commander/events/manager.py +61 -1
  26. claude_mpm/commander/frameworks/base.py +87 -0
  27. claude_mpm/commander/frameworks/mpm.py +9 -14
  28. claude_mpm/commander/git/__init__.py +5 -0
  29. claude_mpm/commander/git/worktree_manager.py +212 -0
  30. claude_mpm/commander/instance_manager.py +428 -13
  31. claude_mpm/commander/models/events.py +6 -0
  32. claude_mpm/commander/persistence/state_store.py +95 -1
  33. claude_mpm/commander/tmux_orchestrator.py +3 -2
  34. claude_mpm/constants.py +5 -0
  35. claude_mpm/core/hook_manager.py +2 -1
  36. claude_mpm/core/logging_utils.py +4 -2
  37. claude_mpm/core/output_style_manager.py +5 -2
  38. claude_mpm/core/socketio_pool.py +34 -10
  39. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  40. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  41. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  47. claude_mpm/hooks/claude_hooks/installer.py +175 -51
  48. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  49. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  50. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  51. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  52. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  58. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  59. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  60. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  61. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  62. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  63. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  64. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  65. claude_mpm/init.py +21 -14
  66. claude_mpm/mcp/__init__.py +9 -0
  67. claude_mpm/mcp/google_workspace_server.py +610 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  69. claude_mpm/services/command_deployment_service.py +44 -26
  70. claude_mpm/services/hook_installer_service.py +77 -8
  71. claude_mpm/services/mcp_config_manager.py +99 -19
  72. claude_mpm/services/mcp_service_registry.py +294 -0
  73. claude_mpm/services/monitor/server.py +6 -1
  74. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/METADATA +24 -1
  75. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/RECORD +80 -60
  76. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/WHEEL +1 -1
  77. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/entry_points.txt +2 -0
  78. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  80. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,32 @@
1
1
  #!/usr/bin/env python3
2
+ # ==============================================================================
3
+ # CRITICAL: EARLY LOGGING SUPPRESSION - MUST BE FIRST
4
+ # ==============================================================================
5
+ # Suppress ALL logging before any other imports to prevent REPL pollution.
6
+ # The StreamingHandler in logger.py writes carriage returns (\r) and spaces
7
+ # to stderr which pollutes Claude Code's REPL output.
8
+ #
9
+ # This MUST be before any imports that could trigger module-level loggers.
10
+ # ==============================================================================
11
+ import logging as _early_logging
12
+ import sys as _early_sys
13
+
14
+ # Force redirect all logging to NullHandler before any module imports
15
+ # This prevents ANY log output from polluting stdout/stderr during hook execution
16
+ _early_logging.basicConfig(handlers=[_early_logging.NullHandler()], force=True)
17
+ # Also ensure root logger has no handlers that write to stderr
18
+ _early_logging.getLogger().handlers = [_early_logging.NullHandler()]
19
+ # Suppress all loggers by setting a very high level initially
20
+ _early_logging.getLogger().setLevel(_early_logging.CRITICAL + 1)
21
+
22
+ # Clean up namespace to avoid polluting module scope
23
+ del _early_logging
24
+ del _early_sys
25
+
26
+ # ==============================================================================
27
+ # END EARLY LOGGING SUPPRESSION
28
+ # ==============================================================================
29
+
2
30
  """Refactored Claude Code hook handler with modular service architecture.
3
31
 
4
32
  This handler uses a service-oriented architecture with:
@@ -17,6 +45,12 @@ NOTE: Requires Claude Code version 1.0.92 or higher for proper hook support.
17
45
  Earlier versions do not support matcher-based hook configuration.
18
46
  """
19
47
 
48
+ # Suppress RuntimeWarning from frozen runpy (prevents REPL pollution in Claude Code)
49
+ # Must be before other imports to suppress warnings during import
50
+ import warnings
51
+
52
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
53
+
20
54
  import json
21
55
  import os
22
56
  import re
@@ -38,6 +72,7 @@ try:
38
72
  from .services import (
39
73
  ConnectionManagerService,
40
74
  DuplicateEventDetector,
75
+ HookServiceContainer,
41
76
  StateManagerService,
42
77
  SubagentResponseProcessor,
43
78
  )
@@ -55,10 +90,26 @@ except ImportError:
55
90
  from services import (
56
91
  ConnectionManagerService,
57
92
  DuplicateEventDetector,
93
+ HookServiceContainer,
58
94
  StateManagerService,
59
95
  SubagentResponseProcessor,
60
96
  )
61
97
 
98
+ # Import CorrelationManager with fallback (used in _route_event cleanup)
99
+ # WHY at top level: Runtime relative imports fail with "no known parent package" error
100
+ try:
101
+ from .correlation_manager import CorrelationManager
102
+ except ImportError:
103
+ try:
104
+ from correlation_manager import CorrelationManager
105
+ except ImportError:
106
+ # Fallback: create a no-op class if module unavailable
107
+ class CorrelationManager:
108
+ @staticmethod
109
+ def cleanup_old():
110
+ pass
111
+
112
+
62
113
  """
63
114
  Debug mode configuration for hook processing.
64
115
 
@@ -228,35 +279,69 @@ class ClaudeHookHandler:
228
279
  - Each service handles a specific responsibility
229
280
  - Easier to test, maintain, and extend
230
281
  - Reduced complexity in main handler class
282
+
283
+ Supports Dependency Injection:
284
+ - Pass a HookServiceContainer to override default services
285
+ - Useful for testing with mock services
286
+ - Maintains backward compatibility when no container is provided
231
287
  """
232
288
 
233
- def __init__(self):
234
- # Initialize services
235
- self.state_manager = StateManagerService()
236
- self.connection_manager = ConnectionManagerService()
237
- self.duplicate_detector = DuplicateEventDetector()
289
+ def __init__(self, container: Optional[HookServiceContainer] = None):
290
+ """Initialize hook handler with optional DI container.
238
291
 
239
- # Initialize extracted managers
240
- self.memory_hook_manager = MemoryHookManager()
241
- self.response_tracking_manager = ResponseTrackingManager()
242
- self.event_handlers = EventHandlers(self)
292
+ Args:
293
+ container: Optional HookServiceContainer for dependency injection.
294
+ If None, services are created directly (backward compatible).
295
+ """
296
+ # Use container if provided, otherwise create services directly
297
+ if container is not None:
298
+ # DI mode: get services from container
299
+ self._container = container
300
+ self.state_manager = container.get_state_manager()
301
+ self.connection_manager = container.get_connection_manager()
302
+ self.duplicate_detector = container.get_duplicate_detector()
303
+ self.memory_hook_manager = container.get_memory_hook_manager()
304
+ self.response_tracking_manager = container.get_response_tracking_manager()
305
+ self.auto_pause_handler = container.get_auto_pause_handler()
306
+
307
+ # Event handlers need reference to this handler (circular, but contained)
308
+ self.event_handlers = EventHandlers(self)
309
+
310
+ # Subagent processor with injected dependencies
311
+ self.subagent_processor = container.get_subagent_processor(
312
+ self.state_manager,
313
+ self.response_tracking_manager,
314
+ self.connection_manager,
315
+ )
316
+ else:
317
+ # Backward compatible mode: create services directly
318
+ self._container = None
319
+ self.state_manager = StateManagerService()
320
+ self.connection_manager = ConnectionManagerService()
321
+ self.duplicate_detector = DuplicateEventDetector()
322
+
323
+ # Initialize extracted managers
324
+ self.memory_hook_manager = MemoryHookManager()
325
+ self.response_tracking_manager = ResponseTrackingManager()
326
+ self.event_handlers = EventHandlers(self)
327
+
328
+ # Initialize subagent processor with dependencies
329
+ self.subagent_processor = SubagentResponseProcessor(
330
+ self.state_manager,
331
+ self.response_tracking_manager,
332
+ self.connection_manager,
333
+ )
243
334
 
244
- # Initialize subagent processor with dependencies
245
- self.subagent_processor = SubagentResponseProcessor(
246
- self.state_manager, self.response_tracking_manager, self.connection_manager
247
- )
335
+ # Initialize auto-pause handler
336
+ try:
337
+ self.auto_pause_handler = AutoPauseHandler()
338
+ except Exception as e:
339
+ self.auto_pause_handler = None
340
+ _log(f"Auto-pause initialization failed: {e}")
248
341
 
249
- # Initialize auto-pause handler
250
- try:
251
- self.auto_pause_handler = AutoPauseHandler()
252
- # Pass reference to ResponseTrackingManager so it can call auto_pause
253
- if hasattr(self, "response_tracking_manager"):
254
- self.response_tracking_manager.auto_pause_handler = (
255
- self.auto_pause_handler
256
- )
257
- except Exception as e:
258
- self.auto_pause_handler = None
259
- _log(f"Auto-pause initialization failed: {e}")
342
+ # Link auto-pause handler to response tracking manager
343
+ if self.auto_pause_handler and hasattr(self, "response_tracking_manager"):
344
+ self.response_tracking_manager.auto_pause_handler = self.auto_pause_handler
260
345
 
261
346
  # Backward compatibility properties for tests
262
347
  # Note: HTTP-based connection manager doesn't use connection_pool
@@ -329,8 +414,6 @@ class ClaudeHookHandler:
329
414
  if self.state_manager.increment_events_processed():
330
415
  self.state_manager.cleanup_old_entries()
331
416
  # Also cleanup old correlation files
332
- from .correlation_manager import CorrelationManager
333
-
334
417
  CorrelationManager.cleanup_old()
335
418
  _log(
336
419
  f"🧹 Performed cleanup after {self.state_manager.events_processed} events"
@@ -496,11 +579,11 @@ class ClaudeHookHandler:
496
579
  if modified_input is not None:
497
580
  # Claude Code v2.0.30+ supports modifying PreToolUse tool inputs
498
581
  print(
499
- json.dumps({"action": "continue", "tool_input": modified_input}),
582
+ json.dumps({"continue": True, "tool_input": modified_input}),
500
583
  flush=True,
501
584
  )
502
585
  else:
503
- print(json.dumps({"action": "continue"}), flush=True)
586
+ print(json.dumps({"continue": True}), flush=True)
504
587
 
505
588
  # Delegation methods for compatibility with event_handlers
506
589
  def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
@@ -673,7 +756,7 @@ def main():
673
756
  # This prevents errors on older Claude Code versions
674
757
  if version:
675
758
  _log(f"Skipping hook processing due to version incompatibility ({version})")
676
- print(json.dumps({"action": "continue"}), flush=True)
759
+ print(json.dumps({"continue": True}), flush=True)
677
760
  sys.exit(0)
678
761
 
679
762
  def cleanup_handler(signum=None, frame=None):
@@ -682,7 +765,7 @@ def main():
682
765
  _log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
683
766
  # Only output continue if we haven't already (i.e., if interrupted by signal)
684
767
  if signum is not None and not _continue_printed:
685
- print(json.dumps({"action": "continue"}), flush=True)
768
+ print(json.dumps({"continue": True}), flush=True)
686
769
  _continue_printed = True
687
770
  sys.exit(0)
688
771
 
@@ -715,7 +798,7 @@ def main():
715
798
  except Exception as e:
716
799
  # Only output continue if not already printed
717
800
  if not _continue_printed:
718
- print(json.dumps({"action": "continue"}), flush=True)
801
+ print(json.dumps({"continue": True}), flush=True)
719
802
  _continue_printed = True
720
803
  # Log error for debugging
721
804
  _log(f"Hook handler error: {e}")
@@ -727,5 +810,5 @@ if __name__ == "__main__":
727
810
  main()
728
811
  except Exception:
729
812
  # Catastrophic failure (import error, etc.) - always output valid JSON
730
- print(json.dumps({"action": "continue"}), flush=True)
813
+ print(json.dumps({"continue": True}), flush=True)
731
814
  sys.exit(0)
@@ -149,7 +149,7 @@ main() {
149
149
  if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
150
150
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Claude MPM not found, continuing..." >> /tmp/claude-mpm-hook.log
151
151
  fi
152
- echo '{"action": "continue"}'
152
+ echo '{"continue": true}'
153
153
  exit 0
154
154
  fi
155
155
 
@@ -176,7 +176,7 @@ main() {
176
176
  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
177
177
  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
178
178
  fi
179
- echo '{"action": "continue"}'
179
+ echo '{"continue": true}'
180
180
  exit 0
181
181
  fi
182
182
 
@@ -203,10 +203,14 @@ main "$@"
203
203
  import logging
204
204
 
205
205
  self.logger = logging.getLogger(__name__)
206
- self.claude_dir = Path.home() / ".claude"
206
+ # Use project-level paths, NEVER global ~/.claude/settings.json
207
+ # This ensures hooks are scoped to the current project only
208
+ self.project_root = Path.cwd()
209
+ self.claude_dir = self.project_root / ".claude"
207
210
  self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
208
- # Use settings.json for hooks (Claude Code reads from this file)
209
- self.settings_file = self.claude_dir / "settings.json"
211
+ # Use settings.local.json for project-level hook settings
212
+ # Claude Code reads project-level settings from .claude/settings.local.json
213
+ self.settings_file = self.claude_dir / "settings.local.json"
210
214
  # There is no legacy settings file - this was a bug where both pointed to same file
211
215
  # Setting to None to disable cleanup that was deleting freshly installed hooks
212
216
  self.old_settings_file = None
@@ -383,8 +387,35 @@ main "$@"
383
387
  return False
384
388
  return self._version_meets_minimum(version, self.MIN_SKILLS_VERSION)
385
389
 
386
- def get_hook_script_path(self) -> Path:
387
- """Get the path to the hook handler script based on installation method.
390
+ def get_hook_command(self) -> str:
391
+ """Get the hook command based on installation method.
392
+
393
+ Priority order:
394
+ 1. claude-hook entry point (uv tool install, pipx install, pip install)
395
+ 2. Fallback to bash script (development installs)
396
+
397
+ Returns:
398
+ Command string for the hook handler (either 'claude-hook' or path to bash script)
399
+
400
+ Raises:
401
+ FileNotFoundError: If no hook handler can be found
402
+ """
403
+ # Check if claude-hook entry point is available in PATH
404
+ claude_hook_path = shutil.which("claude-hook")
405
+ if claude_hook_path:
406
+ self.logger.info(f"Using claude-hook entry point: {claude_hook_path}")
407
+ return "claude-hook"
408
+
409
+ # Fallback to bash script for development installs
410
+ script_path = self._get_hook_script_path()
411
+ self.logger.info(f"Using fallback bash script: {script_path}")
412
+ return str(script_path.absolute())
413
+
414
+ def _get_hook_script_path(self) -> Path:
415
+ """Get the path to the fallback bash hook handler script.
416
+
417
+ This is used when the claude-hook entry point is not available
418
+ (e.g., development installs without uv tool install).
388
419
 
389
420
  Returns:
390
421
  Path to the claude-hook-handler.sh script
@@ -437,6 +468,19 @@ main "$@"
437
468
 
438
469
  raise FileNotFoundError(f"Hook handler script not found at {script_path}")
439
470
 
471
+ def get_hook_script_path(self) -> Path:
472
+ """Get the path to the hook handler script based on installation method.
473
+
474
+ DEPRECATED: Use get_hook_command() instead for proper entry point support.
475
+
476
+ Returns:
477
+ Path to the claude-hook-handler.sh script
478
+
479
+ Raises:
480
+ FileNotFoundError: If the script cannot be found
481
+ """
482
+ return self._get_hook_script_path()
483
+
440
484
  def install_hooks(self, force: bool = False) -> bool:
441
485
  """
442
486
  Install Claude MPM hooks.
@@ -469,18 +513,16 @@ main "$@"
469
513
  # Create Claude directory (hooks_dir no longer needed)
470
514
  self.claude_dir.mkdir(exist_ok=True)
471
515
 
472
- # Get the deployment-root hook script path
516
+ # Get the hook command (either claude-hook entry point or fallback bash script)
473
517
  try:
474
- hook_script_path = self.get_hook_script_path()
475
- self.logger.info(
476
- f"Using deployment-root hook script: {hook_script_path}"
477
- )
518
+ hook_command = self.get_hook_command()
519
+ self.logger.info(f"Using hook command: {hook_command}")
478
520
  except FileNotFoundError as e:
479
- self.logger.error(f"Failed to locate hook script: {e}")
521
+ self.logger.error(f"Failed to locate hook handler: {e}")
480
522
  return False
481
523
 
482
- # Update Claude settings to use deployment-root script
483
- self._update_claude_settings(hook_script_path)
524
+ # Update Claude settings to use the hook command
525
+ self._update_claude_settings(hook_command)
484
526
 
485
527
  # Install commands if available
486
528
  self._install_commands()
@@ -575,8 +617,12 @@ main "$@"
575
617
  "StatusLine command already supports both schemas or not present"
576
618
  )
577
619
 
578
- def _update_claude_settings(self, hook_script_path: Path) -> None:
579
- """Update Claude settings to use the installed hook."""
620
+ def _update_claude_settings(self, hook_cmd: str) -> None:
621
+ """Update Claude settings to use the installed hook.
622
+
623
+ Args:
624
+ hook_cmd: The hook command to use (either 'claude-hook' or path to bash script)
625
+ """
580
626
  self.logger.info("Updating Claude settings...")
581
627
 
582
628
  # Load existing settings.json or create new
@@ -599,42 +645,104 @@ main "$@"
599
645
  settings["hooks"] = {}
600
646
 
601
647
  # Hook configuration for each event type
602
- hook_command = {"type": "command", "command": str(hook_script_path.absolute())}
648
+ hook_command = {"type": "command", "command": hook_cmd}
649
+
650
+ def is_our_hook(cmd: dict) -> bool:
651
+ """Check if a hook command belongs to claude-mpm."""
652
+ if cmd.get("type") != "command":
653
+ return False
654
+ command = cmd.get("command", "")
655
+ # Match claude-hook entry point or bash script fallback
656
+ return (
657
+ command == "claude-hook"
658
+ or "claude-hook-handler.sh" in command
659
+ or command.endswith("claude-mpm-hook.sh")
660
+ )
661
+
662
+ def merge_hooks_for_event(
663
+ existing_hooks: list, new_hook_command: dict, use_matcher: bool = True
664
+ ) -> list:
665
+ """Merge new hook command into existing hooks without duplication.
666
+
667
+ Args:
668
+ existing_hooks: Current hooks configuration for an event type
669
+ new_hook_command: The claude-mpm hook command to add
670
+ use_matcher: Whether to include matcher: "*" in the config
671
+
672
+ Returns:
673
+ Updated hooks list with our hook merged in
674
+ """
675
+ # Check if our hook already exists in any existing hook config
676
+ our_hook_exists = False
677
+
678
+ for hook_config in existing_hooks:
679
+ if "hooks" in hook_config and isinstance(hook_config["hooks"], list):
680
+ for hook in hook_config["hooks"]:
681
+ if is_our_hook(hook):
682
+ # Update existing hook command path (in case it changed)
683
+ hook["command"] = new_hook_command["command"]
684
+ our_hook_exists = True
685
+ break
686
+ if our_hook_exists:
687
+ break
688
+
689
+ if our_hook_exists:
690
+ # Our hook already exists, just return the updated list
691
+ return existing_hooks
692
+
693
+ # Our hook doesn't exist - need to add it
694
+ # Strategy: Add our hook to the first "*" matcher config, or create new config
695
+ added = False
696
+
697
+ for hook_config in existing_hooks:
698
+ # Check if this config has matcher: "*" (or no matcher for simple events)
699
+ matcher = hook_config.get("matcher")
700
+ if matcher == "*" or (not use_matcher and matcher is None):
701
+ # Add our hook to this config's hooks array
702
+ if "hooks" not in hook_config:
703
+ hook_config["hooks"] = []
704
+ hook_config["hooks"].append(new_hook_command)
705
+ added = True
706
+ break
707
+
708
+ if not added:
709
+ # No suitable config found, create a new one
710
+ if use_matcher:
711
+ new_config = {"matcher": "*", "hooks": [new_hook_command]}
712
+ else:
713
+ new_config = {"hooks": [new_hook_command]}
714
+ existing_hooks.append(new_config)
715
+
716
+ return existing_hooks
603
717
 
604
718
  # Tool-related events need a matcher string
605
719
  tool_events = ["PreToolUse", "PostToolUse"]
606
720
  for event_type in tool_events:
607
- settings["hooks"][event_type] = [
608
- {
609
- "matcher": "*", # String value to match all tools
610
- "hooks": [hook_command],
611
- }
612
- ]
721
+ existing = settings["hooks"].get(event_type, [])
722
+ settings["hooks"][event_type] = merge_hooks_for_event(
723
+ existing, hook_command, use_matcher=True
724
+ )
613
725
 
614
726
  # Simple events (no subtypes, no matcher needed)
615
- simple_events = ["Stop", "SubagentStop", "SubagentStart"]
727
+ # Note: SubagentStart is NOT a valid Claude Code event (only SubagentStop is)
728
+ simple_events = ["Stop", "SubagentStop"]
616
729
  for event_type in simple_events:
617
- settings["hooks"][event_type] = [
618
- {
619
- "hooks": [hook_command],
620
- }
621
- ]
730
+ existing = settings["hooks"].get(event_type, [])
731
+ settings["hooks"][event_type] = merge_hooks_for_event(
732
+ existing, hook_command, use_matcher=False
733
+ )
622
734
 
623
735
  # SessionStart needs matcher for subtypes (startup, resume)
624
- settings["hooks"]["SessionStart"] = [
625
- {
626
- "matcher": "*", # Match all SessionStart subtypes
627
- "hooks": [hook_command],
628
- }
629
- ]
736
+ existing = settings["hooks"].get("SessionStart", [])
737
+ settings["hooks"]["SessionStart"] = merge_hooks_for_event(
738
+ existing, hook_command, use_matcher=True
739
+ )
630
740
 
631
741
  # UserPromptSubmit needs matcher for potential subtypes
632
- settings["hooks"]["UserPromptSubmit"] = [
633
- {
634
- "matcher": "*",
635
- "hooks": [hook_command],
636
- }
637
- ]
742
+ existing = settings["hooks"].get("UserPromptSubmit", [])
743
+ settings["hooks"]["UserPromptSubmit"] = merge_hooks_for_event(
744
+ existing, hook_command, use_matcher=True
745
+ )
638
746
 
639
747
  # Fix statusLine command to handle both output style schemas
640
748
  self._fix_status_line(settings)
@@ -736,10 +844,10 @@ main "$@"
736
844
  issues.append("No hooks configured in Claude settings")
737
845
  else:
738
846
  # Check for required event types
847
+ # Note: SubagentStart is NOT a valid Claude Code event
739
848
  required_events = [
740
849
  "Stop",
741
850
  "SubagentStop",
742
- "SubagentStart",
743
851
  "PreToolUse",
744
852
  "PostToolUse",
745
853
  ]
@@ -805,7 +913,8 @@ main "$@"
805
913
  ):
806
914
  cmd = hook_cmd.get("command", "")
807
915
  if (
808
- "claude-hook-handler.sh" in cmd
916
+ cmd == "claude-hook"
917
+ or "claude-hook-handler.sh" in cmd
809
918
  or cmd.endswith("claude-mpm-hook.sh")
810
919
  ):
811
920
  is_claude_mpm = True
@@ -850,27 +959,42 @@ main "$@"
850
959
 
851
960
  is_valid, issues = self.verify_hooks()
852
961
 
853
- # Try to get deployment-root script path
962
+ # Try to get hook command (entry point or fallback script)
963
+ hook_command = None
964
+ using_entry_point = False
854
965
  try:
855
- hook_script_path = self.get_hook_script_path()
966
+ hook_command = self.get_hook_command()
967
+ using_entry_point = hook_command == "claude-hook"
968
+ except FileNotFoundError:
969
+ hook_command = None
970
+
971
+ # For backward compatibility, also try to get the script path
972
+ hook_script_str = None
973
+ script_exists = False
974
+ try:
975
+ hook_script_path = self._get_hook_script_path()
856
976
  hook_script_str = str(hook_script_path)
857
977
  script_exists = hook_script_path.exists()
858
978
  except FileNotFoundError:
859
- hook_script_str = None
860
- script_exists = False
979
+ pass
861
980
 
862
981
  status = {
863
- "installed": script_exists and self.settings_file.exists(),
982
+ "installed": (hook_command is not None or script_exists)
983
+ and self.settings_file.exists(),
864
984
  "valid": is_valid,
865
985
  "issues": issues,
866
- "hook_script": hook_script_str,
986
+ "hook_command": hook_command,
987
+ "hook_script": hook_script_str, # Kept for backward compatibility
988
+ "using_entry_point": using_entry_point,
867
989
  "settings_file": (
868
990
  str(self.settings_file) if self.settings_file.exists() else None
869
991
  ),
870
992
  "claude_version": claude_version,
871
993
  "version_compatible": is_compatible,
872
994
  "version_message": version_message,
873
- "deployment_type": "deployment-root", # New field to indicate new architecture
995
+ "deployment_type": "entry-point"
996
+ if using_entry_point
997
+ else "deployment-root",
874
998
  "pretool_modify_supported": pretool_modify_supported, # v2.0.30+ feature
875
999
  "pretool_modify_message": (
876
1000
  f"PreToolUse input modification supported (v{claude_version})"
@@ -67,7 +67,7 @@ from datetime import datetime, timezone
67
67
  from typing import Optional
68
68
 
69
69
  # Debug mode
70
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
70
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
71
71
 
72
72
  # Memory hooks integration
73
73
  MEMORY_HOOKS_AVAILABLE = False
@@ -22,7 +22,7 @@ except ImportError:
22
22
 
23
23
 
24
24
  # Debug mode
25
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
25
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
26
26
 
27
27
  # Response tracking integration
28
28
  # NOTE: ResponseTracker import moved to _initialize_response_tracking() for lazy loading
@@ -3,13 +3,34 @@
3
3
  # Use HTTP-based connection manager for stable dashboard communication
4
4
  # from .connection_manager import ConnectionManagerService # Old SocketIO-based
5
5
  from .connection_manager_http import ConnectionManagerService # New HTTP-based
6
+ from .container import HookServiceContainer, get_container
6
7
  from .duplicate_detector import DuplicateEventDetector
8
+ from .protocols import (
9
+ IAutoPauseHandler,
10
+ IConnectionManager,
11
+ IDuplicateDetector,
12
+ IEventHandlers,
13
+ IMemoryHookManager,
14
+ IResponseTrackingManager,
15
+ IStateManager,
16
+ ISubagentProcessor,
17
+ )
7
18
  from .state_manager import StateManagerService
8
19
  from .subagent_processor import SubagentResponseProcessor
9
20
 
10
21
  __all__ = [
11
22
  "ConnectionManagerService",
12
23
  "DuplicateEventDetector",
24
+ "HookServiceContainer",
25
+ "IAutoPauseHandler",
26
+ "IConnectionManager",
27
+ "IDuplicateDetector",
28
+ "IEventHandlers",
29
+ "IMemoryHookManager",
30
+ "IResponseTrackingManager",
31
+ "IStateManager",
32
+ "ISubagentProcessor",
13
33
  "StateManagerService",
14
34
  "SubagentResponseProcessor",
35
+ "get_container",
15
36
  ]
@@ -29,8 +29,8 @@ except ImportError:
29
29
  pass # Silent fallback
30
30
 
31
31
 
32
- # Debug mode is enabled by default for better visibility into hook processing
33
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
32
+ # Debug mode - disabled by default to prevent logging overhead in production
33
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
34
34
 
35
35
  # Import extracted modules with fallback for direct execution
36
36
  try:
@@ -28,8 +28,8 @@ except ImportError:
28
28
  pass # Silent fallback
29
29
 
30
30
 
31
- # Debug mode is enabled by default for better visibility into hook processing
32
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
31
+ # Debug mode - disabled by default to prevent logging overhead in production
32
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
33
33
 
34
34
  # Import requests for HTTP POST communication
35
35
  try: