claude-mpm 5.6.17__py3-none-any.whl → 5.6.33__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/cli/commands/commander.py +7 -7
- claude_mpm/cli/parsers/commander_parser.py +2 -2
- claude_mpm/cli/startup.py +36 -19
- claude_mpm/commander/chat/cli.py +38 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/daemon.py +9 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/core/claude_runner.py +22 -13
- claude_mpm/core/config.py +3 -3
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +13 -5
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.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/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +262 -89
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
- claude_mpm/hooks/claude_hooks/installer.py +90 -28
- 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/__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__/container.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.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/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 +310 -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/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/pm_skills_deployer.py +3 -2
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +1 -1
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +56 -78
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Protocol definitions for hook handler services.
|
|
2
|
+
|
|
3
|
+
This module defines Protocol classes for all hook handler services,
|
|
4
|
+
enabling type-safe dependency injection and easy testing through
|
|
5
|
+
protocol-based duck typing.
|
|
6
|
+
|
|
7
|
+
WHY Protocol-based DI:
|
|
8
|
+
- Enables loose coupling between components
|
|
9
|
+
- Allows easy mocking in tests without monkey-patching
|
|
10
|
+
- Provides clear interface contracts
|
|
11
|
+
- Supports static type checking with mypy
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Callable, Optional, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class IStateManager(Protocol):
|
|
19
|
+
"""Protocol for state management service.
|
|
20
|
+
|
|
21
|
+
Manages:
|
|
22
|
+
- Agent delegation tracking
|
|
23
|
+
- Git branch caching
|
|
24
|
+
- Session state management
|
|
25
|
+
- Cleanup of old entries
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
active_delegations: dict
|
|
29
|
+
delegation_history: Any # deque
|
|
30
|
+
delegation_requests: dict
|
|
31
|
+
pending_prompts: dict
|
|
32
|
+
events_processed: int
|
|
33
|
+
|
|
34
|
+
def track_delegation(
|
|
35
|
+
self, session_id: str, agent_type: str, request_data: Optional[dict] = None
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Track a new agent delegation with optional request data."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def get_delegation_agent_type(self, session_id: str) -> str:
|
|
41
|
+
"""Get the agent type for a session's active delegation."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def cleanup_old_entries(self) -> None:
|
|
45
|
+
"""Clean up old entries to prevent memory growth."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def get_git_branch(self, working_dir: Optional[str] = None) -> str:
|
|
49
|
+
"""Get git branch for the given directory with caching."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def find_matching_request(self, session_id: str) -> Optional[dict]:
|
|
53
|
+
"""Find matching request data for a session, with fuzzy matching fallback."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def remove_request(self, session_id: str) -> None:
|
|
57
|
+
"""Remove request data for a session."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def increment_events_processed(self) -> bool:
|
|
61
|
+
"""Increment events processed counter and return True if cleanup is needed."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class IConnectionManager(Protocol):
|
|
67
|
+
"""Protocol for connection management service.
|
|
68
|
+
|
|
69
|
+
Handles:
|
|
70
|
+
- HTTP-based event emission to dashboard
|
|
71
|
+
- Event queuing and batching
|
|
72
|
+
- Connection state management
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def emit_event(self, namespace: str, event: str, data: dict) -> None:
|
|
76
|
+
"""Emit an event through HTTP to the dashboard."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def cleanup(self) -> None:
|
|
80
|
+
"""Clean up any resources."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@runtime_checkable
|
|
85
|
+
class IDuplicateDetector(Protocol):
|
|
86
|
+
"""Protocol for duplicate event detection service.
|
|
87
|
+
|
|
88
|
+
Detects:
|
|
89
|
+
- Duplicate events within a time window
|
|
90
|
+
- Rapid-fire events that should be deduplicated
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def is_duplicate(self, event: dict) -> bool:
|
|
94
|
+
"""Check if an event is a duplicate of a recent event."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
def generate_event_key(self, event: dict) -> str:
|
|
98
|
+
"""Generate a unique key for an event for deduplication."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@runtime_checkable
|
|
103
|
+
class IResponseTrackingManager(Protocol):
|
|
104
|
+
"""Protocol for response tracking management.
|
|
105
|
+
|
|
106
|
+
Manages:
|
|
107
|
+
- Response tracking enablement
|
|
108
|
+
- Comprehensive response logging
|
|
109
|
+
- Agent response correlation
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
response_tracking_enabled: bool
|
|
113
|
+
response_tracker: Optional[Any]
|
|
114
|
+
track_all_interactions: bool
|
|
115
|
+
auto_pause_handler: Optional[Any]
|
|
116
|
+
|
|
117
|
+
def track_agent_response(
|
|
118
|
+
self,
|
|
119
|
+
session_id: str,
|
|
120
|
+
agent_type: str,
|
|
121
|
+
event: dict,
|
|
122
|
+
delegation_requests: dict,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Track an agent response for logging."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def track_stop_response(
|
|
128
|
+
self,
|
|
129
|
+
event: dict,
|
|
130
|
+
session_id: str,
|
|
131
|
+
metadata: dict,
|
|
132
|
+
pending_prompts: dict,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Track a stop event response."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
def track_assistant_response(
|
|
138
|
+
self,
|
|
139
|
+
event: dict,
|
|
140
|
+
pending_prompts: dict,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Track an assistant response."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@runtime_checkable
|
|
147
|
+
class IMemoryHookManager(Protocol):
|
|
148
|
+
"""Protocol for memory hook management.
|
|
149
|
+
|
|
150
|
+
Manages:
|
|
151
|
+
- Pre/post delegation memory hooks
|
|
152
|
+
- Memory field processing
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def trigger_pre_delegation_hook(
|
|
156
|
+
self, agent_type: str, tool_input: dict, session_id: str
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Trigger memory hooks before delegation."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def trigger_post_delegation_hook(
|
|
162
|
+
self, agent_type: str, event: dict, session_id: str
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Trigger memory hooks after delegation."""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@runtime_checkable
|
|
169
|
+
class ISubagentProcessor(Protocol):
|
|
170
|
+
"""Protocol for subagent response processing.
|
|
171
|
+
|
|
172
|
+
Handles:
|
|
173
|
+
- SubagentStop event processing
|
|
174
|
+
- Structured response extraction
|
|
175
|
+
- Response tracking and correlation
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def process_subagent_stop(self, event: dict) -> None:
|
|
179
|
+
"""Handle subagent stop events."""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@runtime_checkable
|
|
184
|
+
class IAutoPauseHandler(Protocol):
|
|
185
|
+
"""Protocol for auto-pause functionality.
|
|
186
|
+
|
|
187
|
+
Manages:
|
|
188
|
+
- Session pause tracking
|
|
189
|
+
- Usage threshold monitoring
|
|
190
|
+
- Session state capture
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def is_pause_active(self) -> bool:
|
|
194
|
+
"""Check if auto-pause is currently active."""
|
|
195
|
+
...
|
|
196
|
+
|
|
197
|
+
def on_user_message(self, message: str) -> None:
|
|
198
|
+
"""Record a user message for auto-pause tracking."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
def on_tool_call(self, tool_name: str, tool_input: dict) -> None:
|
|
202
|
+
"""Record a tool call for auto-pause tracking."""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
def on_assistant_response(self, response: str) -> None:
|
|
206
|
+
"""Record an assistant response for auto-pause tracking."""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
def on_usage_update(self, usage: dict) -> Optional[str]:
|
|
210
|
+
"""Update usage metrics and return threshold crossed if any."""
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
def emit_threshold_warning(self, threshold: str) -> str:
|
|
214
|
+
"""Emit a warning for a crossed threshold."""
|
|
215
|
+
...
|
|
216
|
+
|
|
217
|
+
def on_session_end(self) -> Optional[Any]:
|
|
218
|
+
"""Finalize the current auto-pause session."""
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@runtime_checkable
|
|
223
|
+
class IEventHandlers(Protocol):
|
|
224
|
+
"""Protocol for event handler collection.
|
|
225
|
+
|
|
226
|
+
Provides handlers for:
|
|
227
|
+
- UserPromptSubmit
|
|
228
|
+
- PreToolUse
|
|
229
|
+
- PostToolUse
|
|
230
|
+
- Notification
|
|
231
|
+
- Stop
|
|
232
|
+
- SubagentStop
|
|
233
|
+
- SubagentStart
|
|
234
|
+
- SessionStart
|
|
235
|
+
- AssistantResponse
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def handle_user_prompt_fast(self, event: dict) -> None:
|
|
239
|
+
"""Handle user prompt with comprehensive data capture."""
|
|
240
|
+
...
|
|
241
|
+
|
|
242
|
+
def handle_pre_tool_fast(self, event: dict) -> Optional[dict]:
|
|
243
|
+
"""Handle pre-tool use with comprehensive data capture."""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
def handle_post_tool_fast(self, event: dict) -> None:
|
|
247
|
+
"""Handle post-tool use with comprehensive data capture."""
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
def handle_notification_fast(self, event: dict) -> None:
|
|
251
|
+
"""Handle notification events from Claude."""
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
def handle_stop_fast(self, event: dict) -> None:
|
|
255
|
+
"""Handle stop events when Claude processing stops."""
|
|
256
|
+
...
|
|
257
|
+
|
|
258
|
+
def handle_subagent_stop_fast(self, event: dict) -> None:
|
|
259
|
+
"""Handle subagent stop events."""
|
|
260
|
+
...
|
|
261
|
+
|
|
262
|
+
def handle_subagent_start_fast(self, event: dict) -> None:
|
|
263
|
+
"""Handle SubagentStart events."""
|
|
264
|
+
...
|
|
265
|
+
|
|
266
|
+
def handle_session_start_fast(self, event: dict) -> None:
|
|
267
|
+
"""Handle session start events."""
|
|
268
|
+
...
|
|
269
|
+
|
|
270
|
+
def handle_assistant_response(self, event: dict) -> None:
|
|
271
|
+
"""Handle assistant response events."""
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Type alias for log function
|
|
276
|
+
LogFunction = Callable[[str], None]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@runtime_checkable
|
|
280
|
+
class ILogManager(Protocol):
|
|
281
|
+
"""Protocol for log manager service (optional dependency).
|
|
282
|
+
|
|
283
|
+
Used for logging agent prompts and responses.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
async def log_prompt(
|
|
287
|
+
self, source: str, content: str, metadata: dict
|
|
288
|
+
) -> Optional[Any]:
|
|
289
|
+
"""Log a prompt to the log manager."""
|
|
290
|
+
...
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@runtime_checkable
|
|
294
|
+
class IDelegationDetector(Protocol):
|
|
295
|
+
"""Protocol for delegation pattern detector (optional dependency).
|
|
296
|
+
|
|
297
|
+
Used for detecting delegation anti-patterns in responses.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def detect_user_delegation(self, text: str) -> list[dict]:
|
|
301
|
+
"""Detect delegation patterns in text."""
|
|
302
|
+
...
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@runtime_checkable
|
|
306
|
+
class IEventLog(Protocol):
|
|
307
|
+
"""Protocol for event log service (optional dependency).
|
|
308
|
+
|
|
309
|
+
Used for logging PM violations and other events.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def append_event(
|
|
313
|
+
self,
|
|
314
|
+
event_type: str,
|
|
315
|
+
payload: dict,
|
|
316
|
+
status: str = "pending",
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Append an event to the log."""
|
|
319
|
+
...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@runtime_checkable
|
|
323
|
+
class IConfig(Protocol):
|
|
324
|
+
"""Protocol for configuration service (optional dependency)."""
|
|
325
|
+
|
|
326
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
327
|
+
"""Get a configuration value."""
|
|
328
|
+
...
|
|
@@ -33,8 +33,8 @@ except ImportError:
|
|
|
33
33
|
QUICK_TIMEOUT = 2.0
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
# Debug mode
|
|
37
|
-
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "
|
|
36
|
+
# Debug mode - disabled by default to prevent logging overhead in production
|
|
37
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class StateManagerService:
|
|
@@ -22,8 +22,8 @@ except ImportError:
|
|
|
22
22
|
pass # Silent fallback
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
# Debug mode
|
|
26
|
-
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "
|
|
25
|
+
# Debug mode - disabled by default to prevent logging overhead in production
|
|
26
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class SubagentResponseProcessor:
|
|
@@ -38,7 +38,7 @@ def main():
|
|
|
38
38
|
# Read event from stdin
|
|
39
39
|
event_data = sys.stdin.read()
|
|
40
40
|
if not event_data.strip():
|
|
41
|
-
print(json.dumps({"
|
|
41
|
+
print(json.dumps({"continue": True}))
|
|
42
42
|
return
|
|
43
43
|
|
|
44
44
|
event = json.loads(event_data)
|
|
@@ -49,7 +49,7 @@ def main():
|
|
|
49
49
|
if tool_name == "Grep" and "-n" not in tool_input:
|
|
50
50
|
modified_input = tool_input.copy()
|
|
51
51
|
modified_input["-n"] = True
|
|
52
|
-
print(json.dumps({"
|
|
52
|
+
print(json.dumps({"continue": True, "tool_input": modified_input}))
|
|
53
53
|
return
|
|
54
54
|
|
|
55
55
|
# Example: Block operations on .env files
|
|
@@ -59,19 +59,19 @@ def main():
|
|
|
59
59
|
print(
|
|
60
60
|
json.dumps(
|
|
61
61
|
{
|
|
62
|
-
"
|
|
63
|
-
"
|
|
62
|
+
"continue": False,
|
|
63
|
+
"stopReason": "Access to .env file blocked for security",
|
|
64
64
|
}
|
|
65
65
|
)
|
|
66
66
|
)
|
|
67
67
|
return
|
|
68
68
|
|
|
69
69
|
# Default: continue without modification
|
|
70
|
-
print(json.dumps({"
|
|
70
|
+
print(json.dumps({"continue": True}))
|
|
71
71
|
|
|
72
72
|
except Exception:
|
|
73
73
|
# Always continue on error to avoid blocking Claude
|
|
74
|
-
print(json.dumps({"
|
|
74
|
+
print(json.dumps({"continue": True}))
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
if __name__ == "__main__":
|
|
@@ -29,7 +29,7 @@ Input Format (stdin):
|
|
|
29
29
|
|
|
30
30
|
Output Format (stdout):
|
|
31
31
|
{
|
|
32
|
-
"
|
|
32
|
+
"continue": true,
|
|
33
33
|
"tool_input": {
|
|
34
34
|
"file_path": "/path/to/file.py",
|
|
35
35
|
"old_string": "foo",
|
|
@@ -39,13 +39,13 @@ Output Format (stdout):
|
|
|
39
39
|
|
|
40
40
|
Or to block execution:
|
|
41
41
|
{
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"continue": false,
|
|
43
|
+
"stopReason": "Reason for blocking"
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
Or to continue without modification:
|
|
47
47
|
{
|
|
48
|
-
"
|
|
48
|
+
"continue": true
|
|
49
49
|
}
|
|
50
50
|
"""
|
|
51
51
|
|
|
@@ -94,14 +94,14 @@ class PreToolUseHook:
|
|
|
94
94
|
self, modified_input: Optional[Dict[str, Any]] = None
|
|
95
95
|
) -> None:
|
|
96
96
|
"""Continue execution with optional modified input."""
|
|
97
|
-
response = {"
|
|
97
|
+
response = {"continue": True}
|
|
98
98
|
if modified_input is not None:
|
|
99
99
|
response["tool_input"] = modified_input
|
|
100
100
|
print(json.dumps(response))
|
|
101
101
|
|
|
102
102
|
def block_execution(self, message: str) -> None:
|
|
103
103
|
"""Block execution with a message."""
|
|
104
|
-
response = {"
|
|
104
|
+
response = {"continue": False, "stopReason": message}
|
|
105
105
|
print(json.dumps(response))
|
|
106
106
|
|
|
107
107
|
def modify_input(
|
|
@@ -222,13 +222,13 @@ log_debug() {
|
|
|
222
222
|
if [[ "$PYTHON_CMD" == "uv run"* ]]; then
|
|
223
223
|
if ! uv run --directory "$CLAUDE_MPM_ROOT" python -c "import claude_mpm" 2>/dev/null; then
|
|
224
224
|
log_debug "claude_mpm module not available, continuing without hook"
|
|
225
|
-
echo '{"
|
|
225
|
+
echo '{"continue": true}'
|
|
226
226
|
exit 0
|
|
227
227
|
fi
|
|
228
228
|
else
|
|
229
229
|
if ! $PYTHON_CMD -c "import claude_mpm" 2>/dev/null; then
|
|
230
230
|
log_debug "claude_mpm module not available, continuing without hook"
|
|
231
|
-
echo '{"
|
|
231
|
+
echo '{"continue": true}'
|
|
232
232
|
exit 0
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
@@ -250,5 +250,5 @@ if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
|
250
250
|
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
|
|
251
251
|
fi
|
|
252
252
|
# Return continue action to prevent blocking Claude Code
|
|
253
|
-
echo '{"
|
|
253
|
+
echo '{"continue": true}'
|
|
254
254
|
exit 0
|
|
File without changes
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
"""Service for
|
|
1
|
+
"""Service for managing MPM slash commands in user's Claude configuration.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
DEPRECATED: User-level commands in ~/.claude/commands/ are deprecated.
|
|
4
|
+
Project-level skills (.claude/skills/) are now the only source for commands.
|
|
5
|
+
|
|
6
|
+
This service now only handles:
|
|
7
|
+
1. Cleanup of deprecated commands from previous versions
|
|
8
|
+
2. Cleanup of stale commands that no longer exist in source
|
|
9
|
+
3. Parsing and validating YAML frontmatter (for internal use)
|
|
10
|
+
|
|
11
|
+
New command deployment is intentionally disabled - see deploy_commands_on_startup().
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from pathlib import Path
|
|
@@ -17,20 +21,34 @@ from claude_mpm.core.logger import get_logger
|
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class CommandDeploymentService(BaseService):
|
|
20
|
-
"""Service for
|
|
24
|
+
"""Service for managing MPM slash commands (cleanup only - deployment deprecated)."""
|
|
21
25
|
|
|
22
|
-
# Deprecated commands that
|
|
26
|
+
# Deprecated commands that should be removed from ~/.claude/commands/
|
|
27
|
+
# ALL user-level commands are now deprecated - project-level skills are the only source
|
|
23
28
|
DEPRECATED_COMMANDS = [
|
|
29
|
+
# Legacy deprecated commands (historical)
|
|
24
30
|
"mpm-agents.md", # Replaced by mpm-agents-list.md
|
|
25
31
|
"mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
|
|
26
32
|
"mpm-config-view.md", # Replaced by mpm-config.md
|
|
27
33
|
"mpm-resume.md", # Replaced by mpm-session-resume.md
|
|
28
34
|
"mpm-ticket.md", # Replaced by mpm-ticket-view.md
|
|
29
|
-
# Removed - consolidated into /mpm-configure
|
|
30
35
|
"mpm-agents-list.md", # Consolidated into /mpm-configure
|
|
31
36
|
"mpm-agents-detect.md", # Consolidated into /mpm-configure
|
|
32
37
|
"mpm-agents-auto-configure.md", # Consolidated into /mpm-configure
|
|
33
38
|
"mpm-agents-recommend.md", # Consolidated into /mpm-configure
|
|
39
|
+
# ALL user-level commands are now deprecated (use project-level skills)
|
|
40
|
+
"mpm.md",
|
|
41
|
+
"mpm-config.md",
|
|
42
|
+
"mpm-doctor.md",
|
|
43
|
+
"mpm-help.md",
|
|
44
|
+
"mpm-init.md",
|
|
45
|
+
"mpm-monitor.md",
|
|
46
|
+
"mpm-organize.md",
|
|
47
|
+
"mpm-postmortem.md",
|
|
48
|
+
"mpm-session-resume.md",
|
|
49
|
+
"mpm-status.md",
|
|
50
|
+
"mpm-ticket-view.md",
|
|
51
|
+
"mpm-version.md",
|
|
34
52
|
]
|
|
35
53
|
|
|
36
54
|
def __init__(self):
|
|
@@ -414,33 +432,33 @@ class CommandDeploymentService(BaseService):
|
|
|
414
432
|
def deploy_commands_on_startup(force: bool = False) -> None:
|
|
415
433
|
"""Convenience function to deploy commands during startup.
|
|
416
434
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
435
|
+
DEPRECATED: User-level commands in ~/.claude/commands/ are deprecated.
|
|
436
|
+
Project-level skills should be the only source for commands.
|
|
437
|
+
|
|
438
|
+
This function now only cleans up any existing deprecated/stale commands
|
|
439
|
+
without deploying new ones.
|
|
421
440
|
|
|
422
441
|
Args:
|
|
423
|
-
force: Force deployment even if files exist
|
|
442
|
+
force: Force deployment even if files exist (ignored - deployment disabled)
|
|
424
443
|
"""
|
|
425
|
-
service = CommandDeploymentService()
|
|
426
444
|
logger = get_logger("startup")
|
|
445
|
+
logger.debug(
|
|
446
|
+
"User-level command deployment is deprecated - "
|
|
447
|
+
"project-level skills are the only command source"
|
|
448
|
+
)
|
|
427
449
|
|
|
428
|
-
#
|
|
450
|
+
# Still clean up any lingering deprecated/stale commands from previous versions
|
|
451
|
+
service = CommandDeploymentService()
|
|
452
|
+
|
|
453
|
+
# Clean up deprecated commands
|
|
429
454
|
deprecated_count = service.remove_deprecated_commands()
|
|
430
455
|
if deprecated_count > 0:
|
|
431
456
|
logger.info(f"Cleaned up {deprecated_count} deprecated command(s)")
|
|
432
457
|
|
|
433
|
-
# Clean up stale commands
|
|
458
|
+
# Clean up stale commands
|
|
434
459
|
stale_count = service.remove_stale_commands()
|
|
435
460
|
if stale_count > 0:
|
|
436
461
|
logger.info(f"Cleaned up {stale_count} stale command(s)")
|
|
437
462
|
|
|
438
|
-
#
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if result["deployed"]:
|
|
442
|
-
logger.info(f"MPM commands deployed: {', '.join(result['deployed'])}")
|
|
443
|
-
|
|
444
|
-
if result["errors"]:
|
|
445
|
-
for error in result["errors"]:
|
|
446
|
-
logger.warning(f"Command deployment issue: {error}")
|
|
463
|
+
# NOTE: Deployment of new commands is intentionally disabled.
|
|
464
|
+
# Project-level skills (.claude/skills/) are the only source for commands.
|
|
@@ -20,8 +20,13 @@ class HookInstallerService:
|
|
|
20
20
|
def __init__(self):
|
|
21
21
|
"""Initialize the hook installer service."""
|
|
22
22
|
self.logger = get_logger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# Use project-level paths, NEVER global ~/.claude/settings.json
|
|
24
|
+
# This ensures hooks are scoped to the current project only
|
|
25
|
+
self.project_root = Path.cwd()
|
|
26
|
+
self.claude_dir = self.project_root / ".claude"
|
|
27
|
+
# Use settings.local.json for project-level hook settings
|
|
28
|
+
# Claude Code reads project-level settings from .claude/settings.local.json
|
|
29
|
+
self.settings_file = self.claude_dir / "settings.local.json"
|
|
25
30
|
|
|
26
31
|
def is_hooks_configured(self) -> bool:
|
|
27
32
|
"""Check if hooks are configured in Claude settings.
|
|
@@ -299,16 +304,77 @@ class HookInstallerService:
|
|
|
299
304
|
self.logger.debug("Creating new Claude settings")
|
|
300
305
|
|
|
301
306
|
# Configure hooks
|
|
302
|
-
|
|
303
|
-
"matcher": "*",
|
|
304
|
-
"hooks": [{"type": "command", "command": hook_script_path}],
|
|
305
|
-
}
|
|
307
|
+
new_hook_command = {"type": "command", "command": hook_script_path}
|
|
306
308
|
|
|
307
309
|
# Update settings
|
|
308
310
|
if "hooks" not in settings:
|
|
309
311
|
settings["hooks"] = {}
|
|
310
312
|
|
|
311
|
-
|
|
313
|
+
def is_our_hook(cmd: Dict[str, Any]) -> bool:
|
|
314
|
+
"""Check if a hook command belongs to claude-mpm."""
|
|
315
|
+
if cmd.get("type") != "command":
|
|
316
|
+
return False
|
|
317
|
+
command = cmd.get("command", "")
|
|
318
|
+
return (
|
|
319
|
+
"hook_wrapper.sh" in command
|
|
320
|
+
or "claude-hook-handler.sh" in command
|
|
321
|
+
or "claude-mpm" in command
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def merge_hooks_for_event(
|
|
325
|
+
existing_hooks: list, hook_command: Dict[str, Any]
|
|
326
|
+
) -> list:
|
|
327
|
+
"""Merge new hook command into existing hooks without duplication.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
existing_hooks: Current hooks configuration for an event type
|
|
331
|
+
hook_command: The claude-mpm hook command to add
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Updated hooks list with our hook merged in
|
|
335
|
+
"""
|
|
336
|
+
# Check if our hook already exists in any existing hook config
|
|
337
|
+
our_hook_exists = False
|
|
338
|
+
|
|
339
|
+
for hook_config in existing_hooks:
|
|
340
|
+
if "hooks" in hook_config and isinstance(
|
|
341
|
+
hook_config["hooks"], list
|
|
342
|
+
):
|
|
343
|
+
for hook in hook_config["hooks"]:
|
|
344
|
+
if is_our_hook(hook):
|
|
345
|
+
# Update existing hook command path (in case it changed)
|
|
346
|
+
hook["command"] = hook_command["command"]
|
|
347
|
+
our_hook_exists = True
|
|
348
|
+
break
|
|
349
|
+
if our_hook_exists:
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
if our_hook_exists:
|
|
353
|
+
# Our hook already exists, just return the updated list
|
|
354
|
+
return existing_hooks
|
|
355
|
+
|
|
356
|
+
# Our hook doesn't exist - need to add it
|
|
357
|
+
# Strategy: Add our hook to the first "*" matcher config, or create new
|
|
358
|
+
added = False
|
|
359
|
+
|
|
360
|
+
for hook_config in existing_hooks:
|
|
361
|
+
# Check if this config has matcher: "*"
|
|
362
|
+
if hook_config.get("matcher") == "*":
|
|
363
|
+
# Add our hook to this config's hooks array
|
|
364
|
+
if "hooks" not in hook_config:
|
|
365
|
+
hook_config["hooks"] = []
|
|
366
|
+
hook_config["hooks"].append(hook_command)
|
|
367
|
+
added = True
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
if not added:
|
|
371
|
+
# No suitable config found, create a new one
|
|
372
|
+
new_config = {"matcher": "*", "hooks": [hook_command]}
|
|
373
|
+
existing_hooks.append(new_config)
|
|
374
|
+
|
|
375
|
+
return existing_hooks
|
|
376
|
+
|
|
377
|
+
# Add hooks for all event types - MERGE instead of overwrite
|
|
312
378
|
for event_type in [
|
|
313
379
|
"UserPromptSubmit",
|
|
314
380
|
"PreToolUse",
|
|
@@ -316,7 +382,10 @@ class HookInstallerService:
|
|
|
316
382
|
"Stop",
|
|
317
383
|
"SubagentStop",
|
|
318
384
|
]:
|
|
319
|
-
settings["hooks"]
|
|
385
|
+
existing = settings["hooks"].get(event_type, [])
|
|
386
|
+
settings["hooks"][event_type] = merge_hooks_for_event(
|
|
387
|
+
existing, new_hook_command
|
|
388
|
+
)
|
|
320
389
|
|
|
321
390
|
# Write settings
|
|
322
391
|
with self.settings_file.open("w") as f:
|