claude-mpm 3.2.1__py3-none-any.whl → 3.3.2__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 (35) hide show
  1. claude_mpm/agents/INSTRUCTIONS.md +71 -2
  2. claude_mpm/agents/templates/data_engineer.json +1 -1
  3. claude_mpm/agents/templates/documentation.json +1 -1
  4. claude_mpm/agents/templates/engineer.json +1 -1
  5. claude_mpm/agents/templates/ops.json +1 -1
  6. claude_mpm/agents/templates/pm.json +1 -1
  7. claude_mpm/agents/templates/qa.json +1 -1
  8. claude_mpm/agents/templates/research.json +1 -1
  9. claude_mpm/agents/templates/security.json +1 -1
  10. claude_mpm/agents/templates/test_integration.json +112 -0
  11. claude_mpm/agents/templates/version_control.json +1 -1
  12. claude_mpm/cli/commands/memory.py +575 -25
  13. claude_mpm/cli/commands/run.py +115 -14
  14. claude_mpm/cli/parser.py +76 -0
  15. claude_mpm/constants.py +5 -0
  16. claude_mpm/core/claude_runner.py +13 -11
  17. claude_mpm/core/session_manager.py +46 -0
  18. claude_mpm/core/simple_runner.py +13 -11
  19. claude_mpm/hooks/claude_hooks/hook_handler.py +2 -26
  20. claude_mpm/scripts/launch_socketio_dashboard.py +261 -0
  21. claude_mpm/services/agent_memory_manager.py +264 -23
  22. claude_mpm/services/memory_builder.py +491 -0
  23. claude_mpm/services/memory_optimizer.py +619 -0
  24. claude_mpm/services/memory_router.py +445 -0
  25. claude_mpm/services/socketio_server.py +389 -1
  26. claude_mpm-3.3.2.dist-info/METADATA +159 -0
  27. {claude_mpm-3.2.1.dist-info → claude_mpm-3.3.2.dist-info}/RECORD +31 -29
  28. claude_mpm/agents/templates/test-integration-agent.md +0 -34
  29. claude_mpm/core/websocket_handler.py +0 -233
  30. claude_mpm/services/websocket_server.py +0 -376
  31. claude_mpm-3.2.1.dist-info/METADATA +0 -432
  32. {claude_mpm-3.2.1.dist-info → claude_mpm-3.3.2.dist-info}/WHEEL +0 -0
  33. {claude_mpm-3.2.1.dist-info → claude_mpm-3.3.2.dist-info}/entry_points.txt +0 -0
  34. {claude_mpm-3.2.1.dist-info → claude_mpm-3.3.2.dist-info}/licenses/LICENSE +0 -0
  35. {claude_mpm-3.2.1.dist-info → claude_mpm-3.3.2.dist-info}/top_level.txt +0 -0
@@ -11,12 +11,13 @@ import sys
11
11
  import time
12
12
  import webbrowser
13
13
  from pathlib import Path
14
+ from datetime import datetime
14
15
 
15
16
  from ...core.logger import get_logger
16
17
  from ...constants import LogLevel
17
18
  from ..utils import get_user_input, list_agent_versions_at_startup
18
19
  from ...utils.dependency_manager import ensure_socketio_dependencies
19
- from ...deployment_paths import get_monitor_html_path, get_scripts_dir, get_package_root
20
+ from ...deployment_paths import get_scripts_dir, get_package_root
20
21
 
21
22
 
22
23
  def filter_claude_mpm_args(claude_args):
@@ -80,10 +81,19 @@ def filter_claude_mpm_args(claude_args):
80
81
  # Also skip the next argument if this flag expects a value
81
82
  value_expecting_flags = {
82
83
  '--websocket-port', '--launch-method', '--logging', '--log-dir',
83
- '--framework-path', '--agents-dir', '-i', '--input', '--resume'
84
+ '--framework-path', '--agents-dir', '-i', '--input'
84
85
  }
86
+ optional_value_flags = {
87
+ '--resume' # These flags can have optional values (nargs="?")
88
+ }
89
+
85
90
  if arg in value_expecting_flags and i < len(claude_args):
86
91
  i += 1 # Skip the value too
92
+ elif arg in optional_value_flags and i < len(claude_args):
93
+ # For optional value flags, only skip next arg if it doesn't start with --
94
+ next_arg = claude_args[i]
95
+ if not next_arg.startswith('--'):
96
+ i += 1 # Skip the value
87
97
  else:
88
98
  # This is not a claude-mpm flag, keep it
89
99
  filtered_args.append(arg)
@@ -92,6 +102,58 @@ def filter_claude_mpm_args(claude_args):
92
102
  return filtered_args
93
103
 
94
104
 
105
+ def create_session_context(session_id, session_manager):
106
+ """
107
+ Create enhanced context for resumed sessions.
108
+
109
+ WHY: When resuming a session, we want to provide Claude with context about
110
+ the previous session including what agents were used and when it was created.
111
+ This helps maintain continuity across session boundaries.
112
+
113
+ Args:
114
+ session_id: Session ID being resumed
115
+ session_manager: SessionManager instance
116
+
117
+ Returns:
118
+ Enhanced context string with session information
119
+ """
120
+ try:
121
+ from ...core.claude_runner import create_simple_context
122
+ except ImportError:
123
+ from claude_mpm.core.claude_runner import create_simple_context
124
+
125
+ base_context = create_simple_context()
126
+
127
+ session_data = session_manager.get_session_by_id(session_id)
128
+ if not session_data:
129
+ return base_context
130
+
131
+ # Add session resumption information
132
+ session_info = f"""
133
+
134
+ # Session Resumption
135
+
136
+ You are resuming session {session_id[:8]}... which was:
137
+ - Created: {session_data.get('created_at', 'unknown')}
138
+ - Last used: {session_data.get('last_used', 'unknown')}
139
+ - Context: {session_data.get('context', 'default')}
140
+ - Use count: {session_data.get('use_count', 0)}
141
+ """
142
+
143
+ # Add information about agents previously run in this session
144
+ agents_run = session_data.get('agents_run', [])
145
+ if agents_run:
146
+ session_info += "\n- Previous agent activity:\n"
147
+ for agent_info in agents_run[-5:]: # Show last 5 agents
148
+ session_info += f" • {agent_info.get('agent', 'unknown')}: {agent_info.get('task', 'no description')[:50]}...\n"
149
+ if len(agents_run) > 5:
150
+ session_info += f" (and {len(agents_run) - 5} other agent interactions)\n"
151
+
152
+ session_info += "\nContinue from where you left off in this session."
153
+
154
+ return base_context + session_info
155
+
156
+
95
157
  def run_session(args):
96
158
  """
97
159
  Run a simplified Claude session.
@@ -112,8 +174,44 @@ def run_session(args):
112
174
 
113
175
  try:
114
176
  from ...core.claude_runner import ClaudeRunner, create_simple_context
177
+ from ...core.session_manager import SessionManager
115
178
  except ImportError:
116
179
  from claude_mpm.core.claude_runner import ClaudeRunner, create_simple_context
180
+ from claude_mpm.core.session_manager import SessionManager
181
+
182
+ # Handle session resumption
183
+ session_manager = SessionManager()
184
+ resume_session_id = None
185
+ resume_context = None
186
+
187
+ if hasattr(args, 'resume') and args.resume:
188
+ if args.resume == "last":
189
+ # Resume the last interactive session
190
+ resume_session_id = session_manager.get_last_interactive_session()
191
+ if resume_session_id:
192
+ session_data = session_manager.get_session_by_id(resume_session_id)
193
+ if session_data:
194
+ resume_context = session_data.get("context", "default")
195
+ logger.info(f"Resuming session {resume_session_id} (context: {resume_context})")
196
+ print(f"🔄 Resuming session {resume_session_id[:8]}... (created: {session_data.get('created_at', 'unknown')})")
197
+ else:
198
+ logger.warning(f"Session {resume_session_id} not found")
199
+ else:
200
+ logger.info("No recent interactive sessions found")
201
+ print("ℹ️ No recent interactive sessions found to resume")
202
+ else:
203
+ # Resume specific session by ID
204
+ resume_session_id = args.resume
205
+ session_data = session_manager.get_session_by_id(resume_session_id)
206
+ if session_data:
207
+ resume_context = session_data.get("context", "default")
208
+ logger.info(f"Resuming session {resume_session_id} (context: {resume_context})")
209
+ print(f"🔄 Resuming session {resume_session_id[:8]}... (context: {resume_context})")
210
+ else:
211
+ logger.error(f"Session {resume_session_id} not found")
212
+ print(f"❌ Session {resume_session_id} not found")
213
+ print("💡 Use 'claude-mpm sessions' to list available sessions")
214
+ return
117
215
 
118
216
  # Skip native agents if disabled
119
217
  if getattr(args, 'no_native_agents', False):
@@ -189,8 +287,19 @@ def run_session(args):
189
287
  # Pass information about whether we already opened the browser in run.py
190
288
  runner._browser_opened_by_cli = getattr(args, '_browser_opened_by_cli', False)
191
289
 
192
- # Create basic context
193
- context = create_simple_context()
290
+ # Create context - use resumed session context if available
291
+ if resume_session_id and resume_context:
292
+ # For resumed sessions, create enhanced context with session information
293
+ context = create_session_context(resume_session_id, session_manager)
294
+ # Update session usage
295
+ session_manager.active_sessions[resume_session_id]["last_used"] = datetime.now().isoformat()
296
+ session_manager.active_sessions[resume_session_id]["use_count"] += 1
297
+ session_manager._save_sessions()
298
+ else:
299
+ # Create a new session for tracking
300
+ new_session_id = session_manager.create_session("default")
301
+ context = create_simple_context()
302
+ logger.info(f"Created new session {new_session_id}")
194
303
 
195
304
  # For monitor mode, we handled everything in launch_socketio_monitor
196
305
  # No need for ClaudeRunner browser delegation
@@ -260,16 +369,8 @@ def launch_socketio_monitor(port, logger):
260
369
 
261
370
  socketio_port = port
262
371
 
263
- # Get path to monitor HTML using deployment paths
264
- html_file_path = get_monitor_html_path()
265
-
266
- if not html_file_path.exists():
267
- logger.error(f"Monitor HTML file not found: {html_file_path}")
268
- print(f"❌ Monitor HTML file not found: {html_file_path}")
269
- return False, False
270
-
271
- # Create file:// URL with port parameter
272
- dashboard_url = f'file://{html_file_path.absolute()}?port={socketio_port}'
372
+ # Use HTTP URL to access dashboard from Socket.IO server
373
+ dashboard_url = f'http://localhost:{socketio_port}'
273
374
 
274
375
  # Check if Socket.IO server is already running
275
376
  server_running = _check_socketio_server_running(socketio_port, logger)
claude_mpm/cli/parser.py CHANGED
@@ -119,6 +119,13 @@ def add_run_arguments(parser: argparse.ArgumentParser) -> None:
119
119
  default=8765,
120
120
  help="WebSocket server port (default: 8765)"
121
121
  )
122
+ run_group.add_argument(
123
+ "--resume",
124
+ type=str,
125
+ nargs="?",
126
+ const="last",
127
+ help="Resume a session (last session if no ID specified, or specific session ID)"
128
+ )
122
129
 
123
130
  # Input/output options
124
131
  io_group = parser.add_argument_group('input/output options')
@@ -214,6 +221,13 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp
214
221
  default=8765,
215
222
  help="WebSocket server port (default: 8765)"
216
223
  )
224
+ run_group.add_argument(
225
+ "--resume",
226
+ type=str,
227
+ nargs="?",
228
+ const="last",
229
+ help="Resume a session (last session if no ID specified, or specific session ID)"
230
+ )
217
231
 
218
232
  # Input/output options
219
233
  io_group = parser.add_argument_group('input/output options (when no command specified)')
@@ -393,6 +407,68 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp
393
407
  help="Clean up old/unused memory files"
394
408
  )
395
409
 
410
+ # Optimize command
411
+ optimize_parser = memory_subparsers.add_parser(
412
+ MemoryCommands.OPTIMIZE.value,
413
+ help="Optimize memory files by removing duplicates and consolidating similar items"
414
+ )
415
+ optimize_parser.add_argument(
416
+ "agent_id",
417
+ nargs="?",
418
+ help="Agent ID to optimize (optimize all if not specified)"
419
+ )
420
+
421
+ # Build command
422
+ build_parser = memory_subparsers.add_parser(
423
+ MemoryCommands.BUILD.value,
424
+ help="Build agent memories from project documentation"
425
+ )
426
+ build_parser.add_argument(
427
+ "--force-rebuild",
428
+ action="store_true",
429
+ help="Force rebuild even if docs haven't changed"
430
+ )
431
+
432
+ # Cross-reference command
433
+ cross_ref_parser = memory_subparsers.add_parser(
434
+ MemoryCommands.CROSS_REF.value,
435
+ help="Find cross-references and common patterns across agent memories"
436
+ )
437
+ cross_ref_parser.add_argument(
438
+ "--query",
439
+ type=str,
440
+ help="Optional search query to filter cross-references"
441
+ )
442
+
443
+ # Route command
444
+ route_parser = memory_subparsers.add_parser(
445
+ MemoryCommands.ROUTE.value,
446
+ help="Test memory command routing logic"
447
+ )
448
+ route_parser.add_argument(
449
+ "--content",
450
+ type=str,
451
+ required=True,
452
+ help="Content to analyze for routing"
453
+ )
454
+
455
+ # Show command
456
+ show_parser = memory_subparsers.add_parser(
457
+ MemoryCommands.SHOW.value,
458
+ help="Show agent memories in user-friendly format with cross-references"
459
+ )
460
+ show_parser.add_argument(
461
+ "agent_id",
462
+ nargs="?",
463
+ help="Agent ID to show memory for (show all if not specified)"
464
+ )
465
+ show_parser.add_argument(
466
+ "--format",
467
+ choices=["summary", "detailed", "full"],
468
+ default="summary",
469
+ help="Display format: summary (default), detailed, or full"
470
+ )
471
+
396
472
  return parser
397
473
 
398
474
 
claude_mpm/constants.py CHANGED
@@ -62,6 +62,11 @@ class MemoryCommands(str, Enum):
62
62
  VIEW = "view"
63
63
  ADD = "add"
64
64
  CLEAN = "clean"
65
+ OPTIMIZE = "optimize"
66
+ BUILD = "build"
67
+ CROSS_REF = "cross-ref"
68
+ ROUTE = "route"
69
+ SHOW = "show"
65
70
 
66
71
 
67
72
  class CLIFlags(str, Enum):
@@ -95,7 +95,7 @@ class ClaudeRunner:
95
95
  except Exception as e:
96
96
  self.logger.debug(f"Failed to create session log file: {e}")
97
97
 
98
- # Initialize WebSocket server reference
98
+ # Initialize Socket.IO server reference
99
99
  self.websocket_server = None
100
100
 
101
101
  def setup_agents(self) -> bool:
@@ -152,13 +152,14 @@ class ClaudeRunner:
152
152
 
153
153
  def run_interactive(self, initial_context: Optional[str] = None):
154
154
  """Run Claude in interactive mode."""
155
- # Start WebSocket server if enabled
155
+ # Connect to Socket.IO server if enabled
156
156
  if self.enable_websocket:
157
157
  try:
158
- # Lazy import to avoid circular dependencies
159
- from claude_mpm.services.websocket_server import WebSocketServer
160
- self.websocket_server = WebSocketServer(port=self.websocket_port)
158
+ # Use Socket.IO client proxy to connect to monitoring server
159
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
160
+ self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
161
161
  self.websocket_server.start()
162
+ self.logger.info("Connected to Socket.IO monitoring server")
162
163
 
163
164
  # Generate session ID
164
165
  session_id = str(uuid.uuid4())
@@ -171,7 +172,7 @@ class ClaudeRunner:
171
172
  working_dir=working_dir
172
173
  )
173
174
  except Exception as e:
174
- self.logger.warning(f"Failed to start WebSocket server: {e}")
175
+ self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
175
176
  self.websocket_server = None
176
177
 
177
178
  # Get version
@@ -329,13 +330,14 @@ class ClaudeRunner:
329
330
  """Run Claude with a single prompt and return success status."""
330
331
  start_time = time.time()
331
332
 
332
- # Start WebSocket server if enabled
333
+ # Connect to Socket.IO server if enabled
333
334
  if self.enable_websocket:
334
335
  try:
335
- # Lazy import to avoid circular dependencies
336
- from claude_mpm.services.websocket_server import WebSocketServer
337
- self.websocket_server = WebSocketServer(port=self.websocket_port)
336
+ # Use Socket.IO client proxy to connect to monitoring server
337
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
338
+ self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
338
339
  self.websocket_server.start()
340
+ self.logger.info("Connected to Socket.IO monitoring server")
339
341
 
340
342
  # Generate session ID
341
343
  session_id = str(uuid.uuid4())
@@ -348,7 +350,7 @@ class ClaudeRunner:
348
350
  working_dir=working_dir
349
351
  )
350
352
  except Exception as e:
351
- self.logger.warning(f"Failed to start WebSocket server: {e}")
353
+ self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
352
354
  self.websocket_server = None
353
355
 
354
356
  # Check for /mpm: commands
@@ -117,6 +117,52 @@ class SessionManager:
117
117
  if expired:
118
118
  self._save_sessions()
119
119
 
120
+ def get_recent_sessions(self, limit: int = 10, context: Optional[str] = None) -> list:
121
+ """Get recent sessions sorted by last used time.
122
+
123
+ Args:
124
+ limit: Maximum number of sessions to return
125
+ context: Filter by context (optional)
126
+
127
+ Returns:
128
+ List of session data dictionaries sorted by last_used descending
129
+ """
130
+ sessions = list(self.active_sessions.values())
131
+
132
+ # Filter by context if specified
133
+ if context:
134
+ sessions = [s for s in sessions if s.get("context") == context]
135
+
136
+ # Sort by last_used descending (most recent first)
137
+ sessions.sort(key=lambda s: datetime.fromisoformat(s["last_used"]), reverse=True)
138
+
139
+ return sessions[:limit]
140
+
141
+ def get_session_by_id(self, session_id: str) -> Optional[Dict[str, Any]]:
142
+ """Get session data by ID.
143
+
144
+ Args:
145
+ session_id: Session ID to look up
146
+
147
+ Returns:
148
+ Session data dictionary or None if not found
149
+ """
150
+ return self.active_sessions.get(session_id)
151
+
152
+ def get_last_interactive_session(self) -> Optional[str]:
153
+ """Get the most recently used interactive session ID.
154
+
155
+ WHY: For --resume without arguments, we want to resume the last
156
+ interactive session (context="default" for regular Claude runs).
157
+
158
+ Returns:
159
+ Session ID of most recent interactive session, or None if none found
160
+ """
161
+ recent_sessions = self.get_recent_sessions(limit=1, context="default")
162
+ if recent_sessions:
163
+ return recent_sessions[0]["id"]
164
+ return None
165
+
120
166
  def _save_sessions(self):
121
167
  """Save sessions to disk."""
122
168
  session_file = self.session_dir / "active_sessions.json"
@@ -95,7 +95,7 @@ class ClaudeRunner:
95
95
  except Exception as e:
96
96
  self.logger.debug(f"Failed to create session log file: {e}")
97
97
 
98
- # Initialize WebSocket server reference
98
+ # Initialize Socket.IO server reference
99
99
  self.websocket_server = None
100
100
 
101
101
  def setup_agents(self) -> bool:
@@ -152,13 +152,14 @@ class ClaudeRunner:
152
152
 
153
153
  def run_interactive(self, initial_context: Optional[str] = None):
154
154
  """Run Claude in interactive mode."""
155
- # Start WebSocket server if enabled
155
+ # Connect to Socket.IO server if enabled
156
156
  if self.enable_websocket:
157
157
  try:
158
- # Lazy import to avoid circular dependencies
159
- from claude_mpm.services.websocket_server import WebSocketServer
160
- self.websocket_server = WebSocketServer(port=self.websocket_port)
158
+ # Use Socket.IO client proxy to connect to monitoring server
159
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
160
+ self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
161
161
  self.websocket_server.start()
162
+ self.logger.info("Connected to Socket.IO monitoring server")
162
163
 
163
164
  # Generate session ID
164
165
  session_id = str(uuid.uuid4())
@@ -171,7 +172,7 @@ class ClaudeRunner:
171
172
  working_dir=working_dir
172
173
  )
173
174
  except Exception as e:
174
- self.logger.warning(f"Failed to start WebSocket server: {e}")
175
+ self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
175
176
  self.websocket_server = None
176
177
 
177
178
  # Get version
@@ -329,13 +330,14 @@ class ClaudeRunner:
329
330
  """Run Claude with a single prompt and return success status."""
330
331
  start_time = time.time()
331
332
 
332
- # Start WebSocket server if enabled
333
+ # Connect to Socket.IO server if enabled
333
334
  if self.enable_websocket:
334
335
  try:
335
- # Lazy import to avoid circular dependencies
336
- from claude_mpm.services.websocket_server import WebSocketServer
337
- self.websocket_server = WebSocketServer(port=self.websocket_port)
336
+ # Use Socket.IO client proxy to connect to monitoring server
337
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
338
+ self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
338
339
  self.websocket_server.start()
340
+ self.logger.info("Connected to Socket.IO monitoring server")
339
341
 
340
342
  # Generate session ID
341
343
  session_id = str(uuid.uuid4())
@@ -348,7 +350,7 @@ class ClaudeRunner:
348
350
  working_dir=working_dir
349
351
  )
350
352
  except Exception as e:
351
- self.logger.warning(f"Failed to start WebSocket server: {e}")
353
+ self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
352
354
  self.websocket_server = None
353
355
 
354
356
  # Check for /mpm: commands
@@ -31,13 +31,7 @@ except ImportError:
31
31
  SOCKETIO_AVAILABLE = False
32
32
  socketio = None
33
33
 
34
- # Fallback imports
35
- try:
36
- from ...services.websocket_server import get_server_instance
37
- SERVER_AVAILABLE = True
38
- except ImportError:
39
- SERVER_AVAILABLE = False
40
- get_server_instance = None
34
+ # No fallback needed - we only use Socket.IO now
41
35
 
42
36
 
43
37
  class ClaudeHookHandler:
@@ -65,14 +59,7 @@ class ClaudeHookHandler:
65
59
  self._git_branch_cache = {}
66
60
  self._git_branch_cache_time = {}
67
61
 
68
- # Initialize fallback server instance if available (but don't start it)
69
- if SERVER_AVAILABLE:
70
- try:
71
- self.websocket_server = get_server_instance()
72
- except:
73
- self.websocket_server = None
74
- else:
75
- self.websocket_server = None
62
+ # No fallback server needed - we only use Socket.IO now
76
63
 
77
64
  def _track_delegation(self, session_id: str, agent_type: str):
78
65
  """Track a new agent delegation."""
@@ -291,17 +278,6 @@ class ClaudeHookHandler:
291
278
  print(f"Socket.IO emit failed: {e}", file=sys.stderr)
292
279
  # Mark as disconnected so next call will reconnect
293
280
  self.sio_connected = False
294
-
295
- # Fallback to legacy WebSocket server
296
- elif hasattr(self, 'websocket_server') and self.websocket_server:
297
- try:
298
- # Map to legacy event format
299
- legacy_event = f"hook.{event}"
300
- self.websocket_server.broadcast_event(legacy_event, data)
301
- if DEBUG:
302
- print(f"Emitted legacy event: {legacy_event}", file=sys.stderr)
303
- except:
304
- pass # Silent failure
305
281
 
306
282
  def _handle_user_prompt_fast(self, event):
307
283
  """Handle user prompt with comprehensive data capture.