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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- 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 +174 -4
- 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/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- 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/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- 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 +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- 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 +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- 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 +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- 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/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {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
|
-
|
|
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)
|
|
@@ -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 '{"
|
|
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 '{"
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
206
|
-
|
|
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
|
|
384
|
-
"""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).
|
|
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
|
|
516
|
+
# Get the hook command (either claude-hook entry point or fallback bash script)
|
|
470
517
|
try:
|
|
471
|
-
|
|
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
|
|
521
|
+
self.logger.error(f"Failed to locate hook handler: {e}")
|
|
477
522
|
return False
|
|
478
523
|
|
|
479
|
-
# Update Claude settings to use
|
|
480
|
-
self._update_claude_settings(
|
|
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
|
|
538
|
-
"""
|
|
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":
|
|
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"]
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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"]
|
|
577
|
-
|
|
578
|
-
|
|
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"]
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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"]
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
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
|
|
962
|
+
# Try to get hook command (entry point or fallback script)
|
|
963
|
+
hook_command = None
|
|
964
|
+
using_entry_point = False
|
|
810
965
|
try:
|
|
811
|
-
|
|
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
|
-
|
|
816
|
-
script_exists = False
|
|
979
|
+
pass
|
|
817
980
|
|
|
818
981
|
status = {
|
|
819
|
-
"installed":
|
|
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
|
-
"
|
|
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": "
|
|
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})"
|