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
@@ -1,4 +1,4 @@
1
- """Simplified Claude runner replacing the complex orchestrator system."""
1
+ """Claude runner with both exec and subprocess launch methods."""
2
2
 
3
3
  import json
4
4
  import os
@@ -8,6 +8,7 @@ import time
8
8
  from datetime import datetime
9
9
  from pathlib import Path
10
10
  from typing import Optional
11
+ import uuid
11
12
 
12
13
  try:
13
14
  from claude_mpm.services.agent_deployment import AgentDeploymentService
@@ -19,28 +20,38 @@ except ImportError:
19
20
  from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
20
21
 
21
22
 
22
- class SimpleClaudeRunner:
23
+ class ClaudeRunner:
23
24
  """
24
- Simplified Claude runner that replaces the entire orchestrator system.
25
+ Claude runner that replaces the entire orchestrator system.
25
26
 
26
27
  This does exactly what we need:
27
28
  1. Deploy native agents to .claude/agents/
28
- 2. Run Claude CLI with basic subprocess calls
29
+ 2. Run Claude CLI with either exec or subprocess
29
30
  3. Extract tickets if needed
30
31
  4. Handle both interactive and non-interactive modes
32
+
33
+ Supports two launch methods:
34
+ - exec: Replace current process (default for backward compatibility)
35
+ - subprocess: Launch as child process for more control
31
36
  """
32
37
 
33
38
  def __init__(
34
39
  self,
35
40
  enable_tickets: bool = True,
36
41
  log_level: str = "OFF",
37
- claude_args: Optional[list] = None
42
+ claude_args: Optional[list] = None,
43
+ launch_method: str = "exec", # "exec" or "subprocess"
44
+ enable_websocket: bool = False,
45
+ websocket_port: int = 8765
38
46
  ):
39
- """Initialize the simple runner."""
47
+ """Initialize the Claude runner."""
40
48
  self.enable_tickets = enable_tickets
41
49
  self.log_level = log_level
42
- self.logger = get_logger("simple_runner")
50
+ self.logger = get_logger("claude_runner")
43
51
  self.claude_args = claude_args or []
52
+ self.launch_method = launch_method
53
+ self.enable_websocket = enable_websocket
54
+ self.websocket_port = websocket_port
44
55
 
45
56
  # Initialize project logger for session logging
46
57
  self.project_logger = None
@@ -48,7 +59,7 @@ class SimpleClaudeRunner:
48
59
  try:
49
60
  self.project_logger = get_project_logger(log_level)
50
61
  self.project_logger.log_system(
51
- "Initializing SimpleClaudeRunner",
62
+ f"Initializing ClaudeRunner with {launch_method} launcher",
52
63
  level="INFO",
53
64
  component="runner"
54
65
  )
@@ -76,12 +87,16 @@ class SimpleClaudeRunner:
76
87
  self.session_log_file = self.project_logger.session_dir / "system.jsonl"
77
88
  self._log_session_event({
78
89
  "event": "session_start",
79
- "runner": "SimpleClaudeRunner",
90
+ "runner": "ClaudeRunner",
80
91
  "enable_tickets": enable_tickets,
81
- "log_level": log_level
92
+ "log_level": log_level,
93
+ "launch_method": launch_method
82
94
  })
83
95
  except Exception as e:
84
96
  self.logger.debug(f"Failed to create session log file: {e}")
97
+
98
+ # Initialize WebSocket server reference
99
+ self.websocket_server = None
85
100
 
86
101
  def setup_agents(self) -> bool:
87
102
  """Deploy native agents to .claude/agents/."""
@@ -137,6 +152,28 @@ class SimpleClaudeRunner:
137
152
 
138
153
  def run_interactive(self, initial_context: Optional[str] = None):
139
154
  """Run Claude in interactive mode."""
155
+ # Start WebSocket server if enabled
156
+ if self.enable_websocket:
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)
161
+ self.websocket_server.start()
162
+
163
+ # Generate session ID
164
+ session_id = str(uuid.uuid4())
165
+ working_dir = os.getcwd()
166
+
167
+ # Notify session start
168
+ self.websocket_server.session_started(
169
+ session_id=session_id,
170
+ launch_method=self.launch_method,
171
+ working_dir=working_dir
172
+ )
173
+ except Exception as e:
174
+ self.logger.warning(f"Failed to start WebSocket server: {e}")
175
+ self.websocket_server = None
176
+
140
177
  # Get version
141
178
  try:
142
179
  from claude_mpm import __version__
@@ -210,17 +247,35 @@ class SimpleClaudeRunner:
210
247
 
211
248
  if self.project_logger:
212
249
  self.project_logger.log_system(
213
- "Launching Claude interactive mode",
250
+ f"Launching Claude interactive mode with {self.launch_method}",
214
251
  level="INFO",
215
252
  component="session"
216
253
  )
217
254
  self._log_session_event({
218
255
  "event": "launching_claude_interactive",
219
- "command": " ".join(cmd)
256
+ "command": " ".join(cmd),
257
+ "method": self.launch_method
220
258
  })
221
259
 
222
- # Replace current process with Claude
223
- os.execvpe(cmd[0], cmd, clean_env)
260
+ # Notify WebSocket clients
261
+ if self.websocket_server:
262
+ self.websocket_server.claude_status_changed(
263
+ status="starting",
264
+ message="Launching Claude interactive session"
265
+ )
266
+
267
+ # Launch using selected method
268
+ if self.launch_method == "subprocess":
269
+ self._launch_subprocess_interactive(cmd, clean_env)
270
+ else:
271
+ # Default to exec for backward compatibility
272
+ if self.websocket_server:
273
+ # Notify before exec (we won't be able to after)
274
+ self.websocket_server.claude_status_changed(
275
+ status="running",
276
+ message="Claude process started (exec mode)"
277
+ )
278
+ os.execvpe(cmd[0], cmd, clean_env)
224
279
 
225
280
  except Exception as e:
226
281
  print(f"Failed to launch Claude: {e}")
@@ -235,6 +290,13 @@ class SimpleClaudeRunner:
235
290
  "error": str(e),
236
291
  "exception_type": type(e).__name__
237
292
  })
293
+
294
+ # Notify WebSocket clients of error
295
+ if self.websocket_server:
296
+ self.websocket_server.claude_status_changed(
297
+ status="error",
298
+ message=f"Failed to launch Claude: {e}"
299
+ )
238
300
  # Fallback to subprocess
239
301
  try:
240
302
  # Use the same clean_env we prepared earlier
@@ -267,6 +329,28 @@ class SimpleClaudeRunner:
267
329
  """Run Claude with a single prompt and return success status."""
268
330
  start_time = time.time()
269
331
 
332
+ # Start WebSocket server if enabled
333
+ if self.enable_websocket:
334
+ 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)
338
+ self.websocket_server.start()
339
+
340
+ # Generate session ID
341
+ session_id = str(uuid.uuid4())
342
+ working_dir = os.getcwd()
343
+
344
+ # Notify session start
345
+ self.websocket_server.session_started(
346
+ session_id=session_id,
347
+ launch_method="oneshot",
348
+ working_dir=working_dir
349
+ )
350
+ except Exception as e:
351
+ self.logger.warning(f"Failed to start WebSocket server: {e}")
352
+ self.websocket_server = None
353
+
270
354
  # Check for /mpm: commands
271
355
  if prompt.strip().startswith("/mpm:"):
272
356
  return self._handle_mpm_command(prompt.strip())
@@ -335,6 +419,13 @@ class SimpleClaudeRunner:
335
419
  component="session"
336
420
  )
337
421
 
422
+ # Notify WebSocket clients
423
+ if self.websocket_server:
424
+ self.websocket_server.claude_status_changed(
425
+ status="running",
426
+ message="Executing Claude oneshot command"
427
+ )
428
+
338
429
  result = subprocess.run(cmd, capture_output=True, text=True, env=env)
339
430
 
340
431
  # Restore original directory if we changed it
@@ -349,6 +440,10 @@ class SimpleClaudeRunner:
349
440
  response = result.stdout.strip()
350
441
  print(response)
351
442
 
443
+ # Broadcast output to WebSocket clients
444
+ if self.websocket_server and response:
445
+ self.websocket_server.claude_output(response, "stdout")
446
+
352
447
  if self.project_logger:
353
448
  # Log successful completion
354
449
  self.project_logger.log_system(
@@ -378,6 +473,17 @@ class SimpleClaudeRunner:
378
473
  "indicators": [p for p in ["Task(", "subagent_type=", "engineer agent", "qa agent"]
379
474
  if p.lower() in response.lower()]
380
475
  })
476
+
477
+ # Notify WebSocket clients about delegation
478
+ if self.websocket_server:
479
+ # Try to extract agent name
480
+ agent_name = self._extract_agent_from_response(response)
481
+ if agent_name:
482
+ self.websocket_server.agent_delegated(
483
+ agent=agent_name,
484
+ task=prompt[:100],
485
+ status="detected"
486
+ )
381
487
 
382
488
  # Extract tickets if enabled
383
489
  if self.enable_tickets and self.ticket_manager and response:
@@ -388,6 +494,14 @@ class SimpleClaudeRunner:
388
494
  error_msg = result.stderr or "Unknown error"
389
495
  print(f"Error: {error_msg}")
390
496
 
497
+ # Broadcast error to WebSocket clients
498
+ if self.websocket_server:
499
+ self.websocket_server.claude_output(error_msg, "stderr")
500
+ self.websocket_server.claude_status_changed(
501
+ status="error",
502
+ message=f"Command failed with code {result.returncode}"
503
+ )
504
+
391
505
  if self.project_logger:
392
506
  self.project_logger.log_system(
393
507
  f"Non-interactive session failed: {error_msg}",
@@ -433,6 +547,14 @@ class SimpleClaudeRunner:
433
547
  )
434
548
  except Exception as e:
435
549
  self.logger.debug(f"Failed to log session summary: {e}")
550
+
551
+ # End WebSocket session
552
+ if self.websocket_server:
553
+ self.websocket_server.claude_status_changed(
554
+ status="stopped",
555
+ message="Session completed"
556
+ )
557
+ self.websocket_server.session_ended()
436
558
 
437
559
  def _extract_tickets(self, text: str):
438
560
  """Extract tickets from Claude's response."""
@@ -519,6 +641,28 @@ class SimpleClaudeRunner:
519
641
  text_lower = text.lower()
520
642
  return any(pattern.lower() in text_lower for pattern in delegation_patterns)
521
643
 
644
+ def _extract_agent_from_response(self, text: str) -> Optional[str]:
645
+ """Try to extract agent name from delegation response."""
646
+ # Look for common patterns
647
+ import re
648
+
649
+ # Pattern 1: subagent_type="agent_name"
650
+ match = re.search(r'subagent_type=["\']([^"\']*)["\'\)]', text)
651
+ if match:
652
+ return match.group(1)
653
+
654
+ # Pattern 2: "engineer agent" etc
655
+ agent_names = [
656
+ "engineer", "qa", "documentation", "research",
657
+ "security", "ops", "version_control", "data_engineer"
658
+ ]
659
+ text_lower = text.lower()
660
+ for agent in agent_names:
661
+ if f"{agent} agent" in text_lower or f"agent: {agent}" in text_lower:
662
+ return agent
663
+
664
+ return None
665
+
522
666
  def _handle_mpm_command(self, prompt: str) -> bool:
523
667
  """Handle /mpm: commands directly without going to Claude."""
524
668
  try:
@@ -594,6 +738,138 @@ class SimpleClaudeRunner:
594
738
  f.write(json.dumps(log_entry) + '\n')
595
739
  except Exception as e:
596
740
  self.logger.debug(f"Failed to log session event: {e}")
741
+
742
+ def _launch_subprocess_interactive(self, cmd: list, env: dict):
743
+ """Launch Claude as a subprocess with PTY for interactive mode."""
744
+ import pty
745
+ import select
746
+ import termios
747
+ import tty
748
+ import signal
749
+
750
+ # Save original terminal settings
751
+ original_tty = None
752
+ if sys.stdin.isatty():
753
+ original_tty = termios.tcgetattr(sys.stdin)
754
+
755
+ # Create PTY
756
+ master_fd, slave_fd = pty.openpty()
757
+
758
+ try:
759
+ # Start Claude process
760
+ process = subprocess.Popen(
761
+ cmd,
762
+ stdin=slave_fd,
763
+ stdout=slave_fd,
764
+ stderr=slave_fd,
765
+ env=env
766
+ )
767
+
768
+ # Close slave in parent
769
+ os.close(slave_fd)
770
+
771
+ if self.project_logger:
772
+ self.project_logger.log_system(
773
+ f"Claude subprocess started with PID {process.pid}",
774
+ level="INFO",
775
+ component="subprocess"
776
+ )
777
+
778
+ # Notify WebSocket clients
779
+ if self.websocket_server:
780
+ self.websocket_server.claude_status_changed(
781
+ status="running",
782
+ pid=process.pid,
783
+ message="Claude subprocess started"
784
+ )
785
+
786
+ # Set terminal to raw mode for proper interaction
787
+ if sys.stdin.isatty():
788
+ tty.setraw(sys.stdin)
789
+
790
+ # Handle Ctrl+C gracefully
791
+ def signal_handler(signum, frame):
792
+ if process.poll() is None:
793
+ process.terminate()
794
+ raise KeyboardInterrupt()
795
+
796
+ signal.signal(signal.SIGINT, signal_handler)
797
+
798
+ # I/O loop
799
+ while True:
800
+ # Check if process is still running
801
+ if process.poll() is not None:
802
+ break
803
+
804
+ # Check for data from Claude or stdin
805
+ r, _, _ = select.select([master_fd, sys.stdin], [], [], 0)
806
+
807
+ if master_fd in r:
808
+ try:
809
+ data = os.read(master_fd, 4096)
810
+ if data:
811
+ os.write(sys.stdout.fileno(), data)
812
+ # Broadcast output to WebSocket clients
813
+ if self.websocket_server:
814
+ try:
815
+ # Decode and send
816
+ output = data.decode('utf-8', errors='replace')
817
+ self.websocket_server.claude_output(output, "stdout")
818
+ except Exception as e:
819
+ self.logger.debug(f"Failed to broadcast output: {e}")
820
+ else:
821
+ break # EOF
822
+ except OSError:
823
+ break
824
+
825
+ if sys.stdin in r:
826
+ try:
827
+ data = os.read(sys.stdin.fileno(), 4096)
828
+ if data:
829
+ os.write(master_fd, data)
830
+ except OSError:
831
+ break
832
+
833
+ # Wait for process to complete
834
+ process.wait()
835
+
836
+ if self.project_logger:
837
+ self.project_logger.log_system(
838
+ f"Claude subprocess exited with code {process.returncode}",
839
+ level="INFO",
840
+ component="subprocess"
841
+ )
842
+
843
+ # Notify WebSocket clients
844
+ if self.websocket_server:
845
+ self.websocket_server.claude_status_changed(
846
+ status="stopped",
847
+ message=f"Claude subprocess exited with code {process.returncode}"
848
+ )
849
+
850
+ finally:
851
+ # Restore terminal
852
+ if original_tty and sys.stdin.isatty():
853
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty)
854
+
855
+ # Close PTY
856
+ try:
857
+ os.close(master_fd)
858
+ except:
859
+ pass
860
+
861
+ # Ensure process is terminated
862
+ if 'process' in locals() and process.poll() is None:
863
+ process.terminate()
864
+ try:
865
+ process.wait(timeout=2)
866
+ except subprocess.TimeoutExpired:
867
+ process.kill()
868
+ process.wait()
869
+
870
+ # End WebSocket session if in subprocess mode
871
+ if self.websocket_server:
872
+ self.websocket_server.session_ended()
597
873
 
598
874
 
599
875
  def create_simple_context() -> str:
@@ -622,10 +898,14 @@ automatically normalize them to lowercase-hyphenated format for the Task tool.
622
898
  Work efficiently and delegate appropriately to subagents when needed."""
623
899
 
624
900
 
901
+ # Backward compatibility alias
902
+ SimpleClaudeRunner = ClaudeRunner
903
+
904
+
625
905
  # Convenience functions for backward compatibility
626
906
  def run_claude_interactive(context: Optional[str] = None):
627
907
  """Run Claude interactively with optional context."""
628
- runner = SimpleClaudeRunner()
908
+ runner = ClaudeRunner()
629
909
  if context is None:
630
910
  context = create_simple_context()
631
911
  runner.run_interactive(context)
@@ -633,7 +913,7 @@ def run_claude_interactive(context: Optional[str] = None):
633
913
 
634
914
  def run_claude_oneshot(prompt: str, context: Optional[str] = None) -> bool:
635
915
  """Run Claude with a single prompt."""
636
- runner = SimpleClaudeRunner()
916
+ runner = ClaudeRunner()
637
917
  if context is None:
638
918
  context = create_simple_context()
639
919
  return runner.run_oneshot(prompt, context)