claude-mpm 5.6.23__py3-none-any.whl → 5.6.73__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +6 -6
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/startup.py +150 -33
- claude_mpm/cli/startup_display.py +3 -2
- claude_mpm/commander/chat/cli.py +5 -2
- claude_mpm/commander/chat/commands.py +42 -16
- claude_mpm/commander/chat/repl.py +1581 -70
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +87 -0
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +428 -13
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/constants.py +5 -0
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +34 -10
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +175 -51
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/METADATA +24 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/RECORD +69 -64
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/entry_points.txt +2 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
#
|
|
250
|
-
|
|
251
|
-
self.auto_pause_handler =
|
|
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({"
|
|
582
|
+
json.dumps({"continue": True, "tool_input": modified_input}),
|
|
500
583
|
flush=True,
|
|
501
584
|
)
|
|
502
585
|
else:
|
|
503
|
-
print(json.dumps({"
|
|
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({"
|
|
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({"
|
|
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({"
|
|
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({"
|
|
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 '{"
|
|
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 '{"
|
|
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
|
-
|
|
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
|
|
209
|
-
|
|
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
|
|
387
|
-
"""Get the
|
|
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
|
|
516
|
+
# Get the hook command (either claude-hook entry point or fallback bash script)
|
|
473
517
|
try:
|
|
474
|
-
|
|
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
|
|
521
|
+
self.logger.error(f"Failed to locate hook handler: {e}")
|
|
480
522
|
return False
|
|
481
523
|
|
|
482
|
-
# Update Claude settings to use
|
|
483
|
-
self._update_claude_settings(
|
|
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,
|
|
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":
|
|
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"]
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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"]
|
|
618
|
-
|
|
619
|
-
|
|
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"]
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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"]
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|
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
|
|
962
|
+
# Try to get hook command (entry point or fallback script)
|
|
963
|
+
hook_command = None
|
|
964
|
+
using_entry_point = False
|
|
854
965
|
try:
|
|
855
|
-
|
|
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
|
-
|
|
860
|
-
script_exists = False
|
|
979
|
+
pass
|
|
861
980
|
|
|
862
981
|
status = {
|
|
863
|
-
"installed":
|
|
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
|
-
"
|
|
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": "
|
|
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", "
|
|
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", "
|
|
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
|
|
33
|
-
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "
|
|
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
|
|
32
|
-
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "
|
|
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:
|