claude-mpm 3.1.2__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/agents/INSTRUCTIONS.md +80 -2
  3. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  4. claude_mpm/agents/base_agent.json +1 -1
  5. claude_mpm/agents/templates/pm.json +25 -0
  6. claude_mpm/agents/templates/research.json +2 -1
  7. claude_mpm/cli/__init__.py +6 -1
  8. claude_mpm/cli/commands/__init__.py +3 -1
  9. claude_mpm/cli/commands/memory.py +232 -0
  10. claude_mpm/cli/commands/run.py +496 -8
  11. claude_mpm/cli/parser.py +91 -1
  12. claude_mpm/config/socketio_config.py +256 -0
  13. claude_mpm/constants.py +9 -0
  14. claude_mpm/core/__init__.py +2 -2
  15. claude_mpm/core/claude_runner.py +919 -0
  16. claude_mpm/core/config.py +21 -1
  17. claude_mpm/core/hook_manager.py +196 -0
  18. claude_mpm/core/pm_hook_interceptor.py +205 -0
  19. claude_mpm/core/simple_runner.py +296 -16
  20. claude_mpm/core/socketio_pool.py +582 -0
  21. claude_mpm/core/websocket_handler.py +233 -0
  22. claude_mpm/deployment_paths.py +261 -0
  23. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  24. claude_mpm/hooks/claude_hooks/hook_handler.py +669 -632
  25. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  26. claude_mpm/hooks/memory_integration_hook.py +312 -0
  27. claude_mpm/orchestration/__init__.py +1 -1
  28. claude_mpm/scripts/claude-mpm-socketio +32 -0
  29. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  30. claude_mpm/scripts/install_socketio_server.py +407 -0
  31. claude_mpm/scripts/launch_monitor.py +132 -0
  32. claude_mpm/scripts/manage_version.py +479 -0
  33. claude_mpm/scripts/socketio_daemon.py +181 -0
  34. claude_mpm/scripts/socketio_server_manager.py +428 -0
  35. claude_mpm/services/__init__.py +5 -0
  36. claude_mpm/services/agent_memory_manager.py +684 -0
  37. claude_mpm/services/hook_service.py +362 -0
  38. claude_mpm/services/socketio_client_manager.py +474 -0
  39. claude_mpm/services/socketio_server.py +698 -0
  40. claude_mpm/services/standalone_socketio_server.py +631 -0
  41. claude_mpm/services/websocket_server.py +376 -0
  42. claude_mpm/utils/dependency_manager.py +211 -0
  43. claude_mpm/web/open_dashboard.py +34 -0
  44. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -1
  45. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +50 -24
  46. claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
  47. claude_mpm/cli_old.py +0 -728
  48. claude_mpm-3.1.2.dist-info/entry_points.txt +0 -4
  49. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  50. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
  51. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
  52. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
claude_mpm/core/config.py CHANGED
@@ -227,7 +227,27 @@ class Config:
227
227
  # Additional configuration
228
228
  "correction_max_file_size_mb": 10,
229
229
  "correction_backup_enabled": True,
230
- "correction_compression_enabled": True
230
+ "correction_compression_enabled": True,
231
+ # Agent Memory System configuration
232
+ "memory": {
233
+ "enabled": True, # Master switch for memory system
234
+ "auto_learning": False, # Automatic learning extraction
235
+ "limits": {
236
+ "default_size_kb": 8, # Default file size limit
237
+ "max_sections": 10, # Maximum sections per file
238
+ "max_items_per_section": 15, # Maximum items per section
239
+ "max_line_length": 120 # Maximum line length
240
+ },
241
+ "agent_overrides": {
242
+ "research": { # Research agent override
243
+ "size_kb": 16, # Can have larger memory
244
+ "auto_learning": True # Enable auto learning
245
+ },
246
+ "qa": { # QA agent override
247
+ "auto_learning": True # Enable auto learning
248
+ }
249
+ }
250
+ }
231
251
  }
232
252
 
233
253
  # Apply defaults for missing keys
@@ -0,0 +1,196 @@
1
+ """Hook manager for manually triggering hook events in PM operations.
2
+
3
+ This module provides a way for the PM agent to manually trigger hook events
4
+ that would normally be handled by Claude Code's hook system. This ensures
5
+ consistency between PM operations and regular agent operations.
6
+
7
+ WHY this is needed:
8
+ - PM runs directly in Python, bypassing Claude Code's hook system
9
+ - TodoWrite and other PM operations should trigger the same hooks as agent operations
10
+ - Ensures consistent event streaming to Socket.IO dashboard
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import subprocess
16
+ import uuid
17
+ from datetime import datetime
18
+ from typing import Dict, Any, Optional
19
+ from pathlib import Path
20
+
21
+ from ..core.logger import get_logger
22
+ from ..deployment_paths import get_package_root
23
+
24
+
25
+ class HookManager:
26
+ """Manager for manually triggering hook events from PM operations.
27
+
28
+ WHY this design:
29
+ - Mimics Claude Code's hook event structure exactly
30
+ - Uses the same hook handler that regular agents use
31
+ - Provides session tracking consistent with regular hook events
32
+ - Enables PM operations to appear in Socket.IO dashboard
33
+ """
34
+
35
+ def __init__(self):
36
+ self.logger = get_logger("hook_manager")
37
+ self.session_id = self._get_or_create_session_id()
38
+ self.hook_handler_path = self._find_hook_handler()
39
+
40
+ def _get_or_create_session_id(self) -> str:
41
+ """Get or create a session ID for hook events."""
42
+ # Try to get session ID from environment (set by ClaudeRunner)
43
+ session_id = os.environ.get('CLAUDE_MPM_SESSION_ID')
44
+ if not session_id:
45
+ # Generate new session ID
46
+ session_id = str(uuid.uuid4())
47
+ os.environ['CLAUDE_MPM_SESSION_ID'] = session_id
48
+ return session_id
49
+
50
+ def _find_hook_handler(self) -> Optional[Path]:
51
+ """Find the hook handler script."""
52
+ try:
53
+ # Look for hook handler in the expected location
54
+ hook_handler = get_package_root() / "hooks" / "claude_hooks" / "hook_handler.py"
55
+
56
+ if hook_handler.exists():
57
+ return hook_handler
58
+ else:
59
+ self.logger.warning(f"Hook handler not found at: {hook_handler}")
60
+ return None
61
+ except Exception as e:
62
+ self.logger.error(f"Error finding hook handler: {e}")
63
+ return None
64
+
65
+ def trigger_pre_tool_hook(self, tool_name: str, tool_args: Dict[str, Any] = None) -> bool:
66
+ """Trigger PreToolUse hook event.
67
+
68
+ Args:
69
+ tool_name: Name of the tool being used (e.g., "TodoWrite")
70
+ tool_args: Arguments passed to the tool
71
+
72
+ Returns:
73
+ bool: True if hook was triggered successfully
74
+ """
75
+ return self._trigger_hook_event("PreToolUse", {
76
+ "tool_name": tool_name,
77
+ "tool_args": tool_args or {},
78
+ "timestamp": datetime.utcnow().isoformat()
79
+ })
80
+
81
+ def trigger_post_tool_hook(self, tool_name: str, exit_code: int = 0, result: Any = None) -> bool:
82
+ """Trigger PostToolUse hook event.
83
+
84
+ Args:
85
+ tool_name: Name of the tool that was used
86
+ exit_code: Exit code (0 for success, non-zero for error)
87
+ result: Result returned by the tool
88
+
89
+ Returns:
90
+ bool: True if hook was triggered successfully
91
+ """
92
+ return self._trigger_hook_event("PostToolUse", {
93
+ "tool_name": tool_name,
94
+ "exit_code": exit_code,
95
+ "result": str(result) if result is not None else None,
96
+ "timestamp": datetime.utcnow().isoformat()
97
+ })
98
+
99
+ def trigger_user_prompt_hook(self, prompt: str) -> bool:
100
+ """Trigger UserPromptSubmit hook event.
101
+
102
+ Args:
103
+ prompt: The user prompt
104
+
105
+ Returns:
106
+ bool: True if hook was triggered successfully
107
+ """
108
+ return self._trigger_hook_event("UserPromptSubmit", {
109
+ "prompt": prompt,
110
+ "timestamp": datetime.utcnow().isoformat()
111
+ })
112
+
113
+ def _trigger_hook_event(self, hook_type: str, event_data: Dict[str, Any]) -> bool:
114
+ """Trigger a hook event by calling the hook handler.
115
+
116
+ Args:
117
+ hook_type: Type of hook event
118
+ event_data: Event data
119
+
120
+ Returns:
121
+ bool: True if hook was triggered successfully
122
+ """
123
+ if not self.hook_handler_path:
124
+ self.logger.debug("Hook handler not available - skipping hook event")
125
+ return False
126
+
127
+ try:
128
+ # Create the hook event in the same format as Claude Code
129
+ hook_event = {
130
+ "hook_event_name": hook_type,
131
+ "session_id": self.session_id,
132
+ "timestamp": datetime.utcnow().isoformat(),
133
+ **event_data
134
+ }
135
+
136
+ # Convert to JSON
137
+ event_json = json.dumps(hook_event)
138
+
139
+ # Call the hook handler
140
+ env = os.environ.copy()
141
+ env['CLAUDE_MPM_HOOK_DEBUG'] = 'true' # Enable debug logging
142
+
143
+ result = subprocess.run(
144
+ ["python", str(self.hook_handler_path)],
145
+ input=event_json,
146
+ text=True,
147
+ capture_output=True,
148
+ env=env,
149
+ timeout=5 # 5 second timeout to prevent hanging
150
+ )
151
+
152
+ if result.returncode == 0:
153
+ self.logger.debug(f"Successfully triggered {hook_type} hook")
154
+ return True
155
+ else:
156
+ self.logger.warning(f"Hook handler returned non-zero exit code: {result.returncode}")
157
+ if result.stderr:
158
+ self.logger.warning(f"Hook handler stderr: {result.stderr}")
159
+ return False
160
+
161
+ except subprocess.TimeoutExpired:
162
+ self.logger.warning(f"Hook handler timed out for {hook_type}")
163
+ return False
164
+ except Exception as e:
165
+ self.logger.error(f"Error triggering {hook_type} hook: {e}")
166
+ return False
167
+
168
+
169
+ # Global instance
170
+ _hook_manager: Optional[HookManager] = None
171
+
172
+
173
+ def get_hook_manager() -> HookManager:
174
+ """Get the global hook manager instance."""
175
+ global _hook_manager
176
+ if _hook_manager is None:
177
+ _hook_manager = HookManager()
178
+ return _hook_manager
179
+
180
+
181
+ def trigger_tool_hooks(tool_name: str, tool_args: Dict[str, Any] = None, result: Any = None, exit_code: int = 0):
182
+ """Convenience function to trigger both pre and post tool hooks.
183
+
184
+ Args:
185
+ tool_name: Name of the tool
186
+ tool_args: Arguments passed to the tool
187
+ result: Result returned by the tool
188
+ exit_code: Exit code (0 for success)
189
+ """
190
+ manager = get_hook_manager()
191
+
192
+ # Trigger pre-tool hook
193
+ manager.trigger_pre_tool_hook(tool_name, tool_args)
194
+
195
+ # Trigger post-tool hook
196
+ manager.trigger_post_tool_hook(tool_name, exit_code, result)
@@ -0,0 +1,205 @@
1
+ """PM Hook Interceptor for TodoWrite operations.
2
+
3
+ This module intercepts TodoWrite operations from the PM agent and ensures
4
+ that the appropriate hook events are triggered, making PM operations
5
+ consistent with regular agent operations in terms of event streaming.
6
+
7
+ WHY this is needed:
8
+ - PM agent runs directly in Python, bypassing Claude Code's hook system
9
+ - TodoWrite calls from PM should trigger the same hooks as agent TodoWrite calls
10
+ - Ensures consistent event streaming to Socket.IO dashboard
11
+ - Maintains compatibility with existing hook-based monitoring systems
12
+ """
13
+
14
+ import functools
15
+ import json
16
+ import os
17
+ import threading
18
+ import time
19
+ from typing import Dict, Any, List, Optional
20
+ from datetime import datetime
21
+
22
+ from ..core.logger import get_logger
23
+ from ..core.hook_manager import get_hook_manager
24
+
25
+
26
+ class PMHookInterceptor:
27
+ """Interceptor for PM operations that ensures hook events are triggered.
28
+
29
+ WHY this design:
30
+ - Acts as a transparent proxy for tool operations
31
+ - Automatically triggers pre/post hook events
32
+ - Maintains session consistency with regular Claude Code operations
33
+ - Provides real-time event streaming for PM operations
34
+ """
35
+
36
+ def __init__(self):
37
+ self.logger = get_logger("pm_hook_interceptor")
38
+ self.hook_manager = get_hook_manager()
39
+ self._in_intercept = threading.local() # Prevent recursion
40
+
41
+ def intercept_todowrite(self, original_function):
42
+ """Decorator to intercept TodoWrite calls and trigger hooks.
43
+
44
+ Args:
45
+ original_function: The original TodoWrite function
46
+
47
+ Returns:
48
+ Wrapped function that triggers hooks
49
+ """
50
+ @functools.wraps(original_function)
51
+ def wrapper(*args, **kwargs):
52
+ # Prevent recursive interception
53
+ if getattr(self._in_intercept, 'active', False):
54
+ return original_function(*args, **kwargs)
55
+
56
+ self._in_intercept.active = True
57
+
58
+ try:
59
+ # Extract todos from arguments
60
+ todos = self._extract_todos_from_args(args, kwargs)
61
+
62
+ # Trigger pre-tool hook
63
+ self.hook_manager.trigger_pre_tool_hook("TodoWrite", {
64
+ "todos": todos,
65
+ "source": "PM",
66
+ "intercepted": True
67
+ })
68
+
69
+ # Call the original function
70
+ result = original_function(*args, **kwargs)
71
+
72
+ # Trigger post-tool hook
73
+ self.hook_manager.trigger_post_tool_hook("TodoWrite", 0, {
74
+ "todos_count": len(todos) if todos else 0,
75
+ "source": "PM",
76
+ "success": True
77
+ })
78
+
79
+ self.logger.debug(f"Successfully intercepted TodoWrite with {len(todos) if todos else 0} todos")
80
+
81
+ return result
82
+
83
+ except Exception as e:
84
+ # Trigger post-tool hook with error
85
+ self.hook_manager.trigger_post_tool_hook("TodoWrite", 1, {
86
+ "error": str(e),
87
+ "source": "PM",
88
+ "success": False
89
+ })
90
+
91
+ self.logger.error(f"Error in TodoWrite interception: {e}")
92
+ raise
93
+ finally:
94
+ self._in_intercept.active = False
95
+
96
+ return wrapper
97
+
98
+ def _extract_todos_from_args(self, args, kwargs) -> List[Dict[str, Any]]:
99
+ """Extract todos from function arguments.
100
+
101
+ Args:
102
+ args: Positional arguments
103
+ kwargs: Keyword arguments
104
+
105
+ Returns:
106
+ List of todo dictionaries
107
+ """
108
+ # Look for todos in kwargs first
109
+ if 'todos' in kwargs:
110
+ return kwargs['todos']
111
+
112
+ # Look for todos in positional args
113
+ for arg in args:
114
+ if isinstance(arg, list) and arg and isinstance(arg[0], dict):
115
+ # Check if this looks like a todos list
116
+ if 'content' in arg[0] or 'id' in arg[0]:
117
+ return arg
118
+
119
+ return []
120
+
121
+ def trigger_manual_todowrite_hooks(self, todos: List[Dict[str, Any]]):
122
+ """Manually trigger TodoWrite hooks for given todos.
123
+
124
+ This method can be called directly when TodoWrite operations
125
+ are detected outside of function interception.
126
+
127
+ Args:
128
+ todos: List of todo dictionaries
129
+ """
130
+ try:
131
+ # Trigger pre-tool hook
132
+ success1 = self.hook_manager.trigger_pre_tool_hook("TodoWrite", {
133
+ "todos": todos,
134
+ "source": "PM_Manual",
135
+ "timestamp": datetime.utcnow().isoformat()
136
+ })
137
+
138
+ # Small delay to ensure proper event ordering
139
+ time.sleep(0.1)
140
+
141
+ # Trigger post-tool hook
142
+ success2 = self.hook_manager.trigger_post_tool_hook("TodoWrite", 0, {
143
+ "todos_count": len(todos),
144
+ "source": "PM_Manual",
145
+ "success": True,
146
+ "timestamp": datetime.utcnow().isoformat()
147
+ })
148
+
149
+ if success1 and success2:
150
+ self.logger.info(f"Manually triggered TodoWrite hooks for {len(todos)} todos")
151
+ else:
152
+ self.logger.warning(f"Hook triggering partially failed: pre={success1}, post={success2}")
153
+
154
+ return success1 and success2
155
+
156
+ except Exception as e:
157
+ self.logger.error(f"Error manually triggering TodoWrite hooks: {e}")
158
+ return False
159
+
160
+
161
+ # Global instance
162
+ _pm_hook_interceptor: Optional[PMHookInterceptor] = None
163
+
164
+
165
+ def get_pm_hook_interceptor() -> PMHookInterceptor:
166
+ """Get the global PM hook interceptor instance."""
167
+ global _pm_hook_interceptor
168
+ if _pm_hook_interceptor is None:
169
+ _pm_hook_interceptor = PMHookInterceptor()
170
+ return _pm_hook_interceptor
171
+
172
+
173
+ def trigger_pm_todowrite_hooks(todos: List[Dict[str, Any]]) -> bool:
174
+ """Convenience function to trigger PM TodoWrite hooks.
175
+
176
+ Args:
177
+ todos: List of todo dictionaries
178
+
179
+ Returns:
180
+ bool: True if hooks were triggered successfully
181
+ """
182
+ interceptor = get_pm_hook_interceptor()
183
+ return interceptor.trigger_manual_todowrite_hooks(todos)
184
+
185
+
186
+ def simulate_pm_todowrite_operation(todos: List[Dict[str, Any]]):
187
+ """Simulate a PM TodoWrite operation with proper hook triggering.
188
+
189
+ This function is useful for testing and for cases where we want to
190
+ simulate a TodoWrite operation from the PM agent.
191
+
192
+ Args:
193
+ todos: List of todo dictionaries
194
+ """
195
+ interceptor = get_pm_hook_interceptor()
196
+
197
+ # Log the operation
198
+ logger = get_logger("pm_todowrite_simulation")
199
+ logger.info(f"Simulating PM TodoWrite operation with {len(todos)} todos")
200
+
201
+ # Trigger hooks
202
+ interceptor.trigger_manual_todowrite_hooks(todos)
203
+
204
+ # Log completion
205
+ logger.info("PM TodoWrite simulation completed")