claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.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)
@@ -14,8 +14,6 @@ import subprocess # nosec B404 - Safe: only uses hardcoded 'claude' CLI command
14
14
  from pathlib import Path
15
15
  from typing import Dict, List, Optional, Tuple
16
16
 
17
- from ...core.logger import get_logger
18
-
19
17
 
20
18
  class HookInstaller:
21
19
  """Manages installation and configuration of Claude MPM hooks."""
@@ -151,7 +149,7 @@ main() {
151
149
  if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
152
150
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Claude MPM not found, continuing..." >> /tmp/claude-mpm-hook.log
153
151
  fi
154
- echo '{"action": "continue"}'
152
+ echo '{"continue": true}'
155
153
  exit 0
156
154
  fi
157
155
 
@@ -178,7 +176,7 @@ main() {
178
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
179
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
180
178
  fi
181
- echo '{"action": "continue"}'
179
+ echo '{"continue": true}'
182
180
  exit 0
183
181
  fi
184
182
 
@@ -199,11 +197,20 @@ main "$@"
199
197
 
200
198
  def __init__(self):
201
199
  """Initialize the hook installer."""
202
- self.logger = get_logger(__name__)
203
- self.claude_dir = Path.home() / ".claude"
200
+ # Use __name__ directly to avoid double prefix
201
+ # __name__ is already 'claude_mpm.hooks.claude_hooks.installer'
202
+ # get_logger() adds 'claude_mpm.' prefix, causing duplicate
203
+ import logging
204
+
205
+ self.logger = logging.getLogger(__name__)
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"
204
210
  self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
205
- # Use settings.json for hooks (Claude Code reads from this file)
206
- 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"
207
214
  # There is no legacy settings file - this was a bug where both pointed to same file
208
215
  # Setting to None to disable cleanup that was deleting freshly installed hooks
209
216
  self.old_settings_file = None
@@ -380,8 +387,35 @@ main "$@"
380
387
  return False
381
388
  return self._version_meets_minimum(version, self.MIN_SKILLS_VERSION)
382
389
 
383
- def get_hook_script_path(self) -> Path:
384
- """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).
385
419
 
386
420
  Returns:
387
421
  Path to the claude-hook-handler.sh script
@@ -434,6 +468,19 @@ main "$@"
434
468
 
435
469
  raise FileNotFoundError(f"Hook handler script not found at {script_path}")
436
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
+
437
484
  def install_hooks(self, force: bool = False) -> bool:
438
485
  """
439
486
  Install Claude MPM hooks.
@@ -466,18 +513,16 @@ main "$@"
466
513
  # Create Claude directory (hooks_dir no longer needed)
467
514
  self.claude_dir.mkdir(exist_ok=True)
468
515
 
469
- # Get the deployment-root hook script path
516
+ # Get the hook command (either claude-hook entry point or fallback bash script)
470
517
  try:
471
- hook_script_path = self.get_hook_script_path()
472
- self.logger.info(
473
- f"Using deployment-root hook script: {hook_script_path}"
474
- )
518
+ hook_command = self.get_hook_command()
519
+ self.logger.info(f"Using hook command: {hook_command}")
475
520
  except FileNotFoundError as e:
476
- self.logger.error(f"Failed to locate hook script: {e}")
521
+ self.logger.error(f"Failed to locate hook handler: {e}")
477
522
  return False
478
523
 
479
- # Update Claude settings to use deployment-root script
480
- self._update_claude_settings(hook_script_path)
524
+ # Update Claude settings to use the hook command
525
+ self._update_claude_settings(hook_command)
481
526
 
482
527
  # Install commands if available
483
528
  self._install_commands()
@@ -534,8 +579,50 @@ main "$@"
534
579
  except Exception as e:
535
580
  self.logger.warning(f"Could not clean up old settings file: {e}")
536
581
 
537
- def _update_claude_settings(self, hook_script_path: Path) -> None:
538
- """Update Claude settings to use the installed hook."""
582
+ def _fix_status_line(self, settings: Dict) -> None:
583
+ """Fix statusLine command to handle both output style schema formats.
584
+
585
+ The statusLine command receives input in different formats:
586
+ - Newer format: {"activeOutputStyle": "Claude MPM", ...}
587
+ - Older format: {"output_style": {"name": "Claude MPM"}, ...}
588
+
589
+ This method ensures the jq expression checks both locations.
590
+
591
+ Args:
592
+ settings: The settings dictionary to update
593
+ """
594
+ if "statusLine" not in settings:
595
+ return
596
+
597
+ status_line = settings.get("statusLine", {})
598
+ if "command" not in status_line:
599
+ return
600
+
601
+ command = status_line["command"]
602
+
603
+ # Pattern to match: '.output_style.name // "default"'
604
+ # We need to update it to: '.output_style.name // .activeOutputStyle // "default"'
605
+ old_pattern = r'\.output_style\.name\s*//\s*"default"'
606
+ new_pattern = '.output_style.name // .activeOutputStyle // "default"'
607
+
608
+ # Check if the command needs updating
609
+ if re.search(old_pattern, command) and ".activeOutputStyle" not in command:
610
+ updated_command = re.sub(old_pattern, new_pattern, command)
611
+ settings["statusLine"]["command"] = updated_command
612
+ self.logger.info(
613
+ "Fixed statusLine command to handle both output style schemas"
614
+ )
615
+ else:
616
+ self.logger.debug(
617
+ "StatusLine command already supports both schemas or not present"
618
+ )
619
+
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
+ """
539
626
  self.logger.info("Updating Claude settings...")
540
627
 
541
628
  # Load existing settings.json or create new
@@ -558,42 +645,107 @@ main "$@"
558
645
  settings["hooks"] = {}
559
646
 
560
647
  # Hook configuration for each event type
561
- 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
562
717
 
563
718
  # Tool-related events need a matcher string
564
719
  tool_events = ["PreToolUse", "PostToolUse"]
565
720
  for event_type in tool_events:
566
- settings["hooks"][event_type] = [
567
- {
568
- "matcher": "*", # String value to match all tools
569
- "hooks": [hook_command],
570
- }
571
- ]
721
+ existing = settings["hooks"].get(event_type, [])
722
+ settings["hooks"][event_type] = merge_hooks_for_event(
723
+ existing, hook_command, use_matcher=True
724
+ )
572
725
 
573
726
  # Simple events (no subtypes, no matcher needed)
574
- simple_events = ["Stop", "SubagentStop", "SubagentStart"]
727
+ # Note: SubagentStart is NOT a valid Claude Code event (only SubagentStop is)
728
+ simple_events = ["Stop", "SubagentStop"]
575
729
  for event_type in simple_events:
576
- settings["hooks"][event_type] = [
577
- {
578
- "hooks": [hook_command],
579
- }
580
- ]
730
+ existing = settings["hooks"].get(event_type, [])
731
+ settings["hooks"][event_type] = merge_hooks_for_event(
732
+ existing, hook_command, use_matcher=False
733
+ )
581
734
 
582
735
  # SessionStart needs matcher for subtypes (startup, resume)
583
- settings["hooks"]["SessionStart"] = [
584
- {
585
- "matcher": "*", # Match all SessionStart subtypes
586
- "hooks": [hook_command],
587
- }
588
- ]
736
+ existing = settings["hooks"].get("SessionStart", [])
737
+ settings["hooks"]["SessionStart"] = merge_hooks_for_event(
738
+ existing, hook_command, use_matcher=True
739
+ )
589
740
 
590
741
  # UserPromptSubmit needs matcher for potential subtypes
591
- settings["hooks"]["UserPromptSubmit"] = [
592
- {
593
- "matcher": "*",
594
- "hooks": [hook_command],
595
- }
596
- ]
742
+ existing = settings["hooks"].get("UserPromptSubmit", [])
743
+ settings["hooks"]["UserPromptSubmit"] = merge_hooks_for_event(
744
+ existing, hook_command, use_matcher=True
745
+ )
746
+
747
+ # Fix statusLine command to handle both output style schemas
748
+ self._fix_status_line(settings)
597
749
 
598
750
  # Write settings to settings.json
599
751
  with self.settings_file.open("w") as f:
@@ -692,10 +844,10 @@ main "$@"
692
844
  issues.append("No hooks configured in Claude settings")
693
845
  else:
694
846
  # Check for required event types
847
+ # Note: SubagentStart is NOT a valid Claude Code event
695
848
  required_events = [
696
849
  "Stop",
697
850
  "SubagentStop",
698
- "SubagentStart",
699
851
  "PreToolUse",
700
852
  "PostToolUse",
701
853
  ]
@@ -761,7 +913,8 @@ main "$@"
761
913
  ):
762
914
  cmd = hook_cmd.get("command", "")
763
915
  if (
764
- "claude-hook-handler.sh" in cmd
916
+ cmd == "claude-hook"
917
+ or "claude-hook-handler.sh" in cmd
765
918
  or cmd.endswith("claude-mpm-hook.sh")
766
919
  ):
767
920
  is_claude_mpm = True
@@ -806,27 +959,42 @@ main "$@"
806
959
 
807
960
  is_valid, issues = self.verify_hooks()
808
961
 
809
- # 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
810
965
  try:
811
- 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()
812
976
  hook_script_str = str(hook_script_path)
813
977
  script_exists = hook_script_path.exists()
814
978
  except FileNotFoundError:
815
- hook_script_str = None
816
- script_exists = False
979
+ pass
817
980
 
818
981
  status = {
819
- "installed": script_exists and self.settings_file.exists(),
982
+ "installed": (hook_command is not None or script_exists)
983
+ and self.settings_file.exists(),
820
984
  "valid": is_valid,
821
985
  "issues": issues,
822
- "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,
823
989
  "settings_file": (
824
990
  str(self.settings_file) if self.settings_file.exists() else None
825
991
  ),
826
992
  "claude_version": claude_version,
827
993
  "version_compatible": is_compatible,
828
994
  "version_message": version_message,
829
- "deployment_type": "deployment-root", # New field to indicate new architecture
995
+ "deployment_type": "entry-point"
996
+ if using_entry_point
997
+ else "deployment-root",
830
998
  "pretool_modify_supported": pretool_modify_supported, # v2.0.30+ feature
831
999
  "pretool_modify_message": (
832
1000
  f"PreToolUse input modification supported (v{claude_version})"