claude-mpm 4.0.17__py3-none-any.whl → 4.0.20__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 (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__main__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
  4. claude_mpm/agents/OUTPUT_STYLE.md +84 -0
  5. claude_mpm/agents/templates/qa.json +24 -12
  6. claude_mpm/cli/__init__.py +85 -1
  7. claude_mpm/cli/__main__.py +4 -0
  8. claude_mpm/cli/commands/mcp_install_commands.py +62 -5
  9. claude_mpm/cli/commands/mcp_server_commands.py +60 -79
  10. claude_mpm/cli/commands/memory.py +32 -5
  11. claude_mpm/cli/commands/run.py +33 -6
  12. claude_mpm/cli/parsers/base_parser.py +5 -0
  13. claude_mpm/cli/parsers/run_parser.py +5 -0
  14. claude_mpm/cli/utils.py +17 -4
  15. claude_mpm/core/base_service.py +1 -1
  16. claude_mpm/core/config.py +70 -5
  17. claude_mpm/core/framework_loader.py +342 -31
  18. claude_mpm/core/interactive_session.py +55 -1
  19. claude_mpm/core/oneshot_session.py +7 -1
  20. claude_mpm/core/output_style_manager.py +468 -0
  21. claude_mpm/core/unified_paths.py +190 -21
  22. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
  23. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
  24. claude_mpm/init.py +1 -0
  25. claude_mpm/scripts/mcp_server.py +68 -0
  26. claude_mpm/scripts/mcp_wrapper.py +39 -0
  27. claude_mpm/services/agents/deployment/agent_deployment.py +151 -7
  28. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
  29. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
  30. claude_mpm/services/agents/memory/__init__.py +0 -2
  31. claude_mpm/services/agents/memory/agent_memory_manager.py +737 -43
  32. claude_mpm/services/agents/memory/content_manager.py +144 -14
  33. claude_mpm/services/agents/memory/template_generator.py +7 -354
  34. claude_mpm/services/mcp_gateway/core/singleton_manager.py +312 -0
  35. claude_mpm/services/mcp_gateway/core/startup_verification.py +315 -0
  36. claude_mpm/services/mcp_gateway/main.py +7 -0
  37. claude_mpm/services/mcp_gateway/server/stdio_server.py +184 -176
  38. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +453 -0
  39. claude_mpm/services/subprocess_launcher_service.py +5 -0
  40. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/METADATA +1 -1
  41. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/RECORD +45 -38
  42. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/entry_points.txt +1 -0
  43. claude_mpm/services/agents/memory/analyzer.py +0 -430
  44. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/WHEEL +0 -0
  45. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/top_level.txt +0 -0
@@ -280,7 +280,12 @@ def _show_basic_status(memory_manager):
280
280
  print(f" Expected location: {memory_dir}")
281
281
  return
282
282
 
283
- memory_files = list(memory_dir.glob("*_agent.md"))
283
+ # Support both old and new formats
284
+ memory_files = list(memory_dir.glob("*_memories.md"))
285
+ # Also check for old formats for backward compatibility
286
+ memory_files.extend(memory_dir.glob("*_agent.md"))
287
+ memory_files.extend([f for f in memory_dir.glob("*.md")
288
+ if f.name != "README.md" and not f.name.endswith("_memories.md") and not f.name.endswith("_agent.md")])
284
289
 
285
290
  if not memory_files:
286
291
  print("📭 No memory files found")
@@ -296,7 +301,13 @@ def _show_basic_status(memory_manager):
296
301
  size_kb = stat.st_size / 1024
297
302
  total_size += stat.st_size
298
303
 
299
- agent_id = file_path.stem.replace("_agent", "")
304
+ # Extract agent name from various formats
305
+ if file_path.name.endswith("_memories.md"):
306
+ agent_id = file_path.stem[:-9] # Remove "_memories"
307
+ elif file_path.name.endswith("_agent.md"):
308
+ agent_id = file_path.stem[:-6] # Remove "_agent"
309
+ else:
310
+ agent_id = file_path.stem
300
311
  print(f" {agent_id}: {size_kb:.1f} KB")
301
312
 
302
313
  print(f"💾 Total size: {total_size / 1024:.1f} KB")
@@ -395,7 +406,12 @@ def _clean_memory(args, memory_manager):
395
406
  print("📁 No memory directory found - nothing to clean")
396
407
  return
397
408
 
398
- memory_files = list(memory_dir.glob("*_agent.md"))
409
+ # Support both old and new formats
410
+ memory_files = list(memory_dir.glob("*_memories.md"))
411
+ # Also check for old formats for backward compatibility
412
+ memory_files.extend(memory_dir.glob("*_agent.md"))
413
+ memory_files.extend([f for f in memory_dir.glob("*.md")
414
+ if f.name != "README.md" and not f.name.endswith("_memories.md") and not f.name.endswith("_agent.md")])
399
415
  if not memory_files:
400
416
  print("📭 No memory files found - nothing to clean")
401
417
  return
@@ -651,7 +667,12 @@ def _show_all_agent_memories(format_type, memory_manager):
651
667
  print("📁 No memory directory found")
652
668
  return
653
669
 
654
- memory_files = list(memory_dir.glob("*_agent.md"))
670
+ # Support both old and new formats
671
+ memory_files = list(memory_dir.glob("*_memories.md"))
672
+ # Also check for old formats for backward compatibility
673
+ memory_files.extend(memory_dir.glob("*_agent.md"))
674
+ memory_files.extend([f for f in memory_dir.glob("*.md")
675
+ if f.name != "README.md" and not f.name.endswith("_memories.md") and not f.name.endswith("_agent.md")])
655
676
  if not memory_files:
656
677
  print("📭 No agent memories found")
657
678
  return
@@ -664,7 +685,13 @@ def _show_all_agent_memories(format_type, memory_manager):
664
685
 
665
686
  # Load all agent memories
666
687
  for file_path in sorted(memory_files):
667
- agent_id = file_path.stem.replace("_agent", "")
688
+ # Extract agent name from various formats
689
+ if file_path.name.endswith("_memories.md"):
690
+ agent_id = file_path.stem[:-9] # Remove "_memories"
691
+ elif file_path.name.endswith("_agent.md"):
692
+ agent_id = file_path.stem[:-6] # Remove "_agent"
693
+ else:
694
+ agent_id = file_path.stem
668
695
  try:
669
696
  memory_content = memory_manager.load_agent_memory(agent_id)
670
697
  if memory_content:
@@ -33,7 +33,8 @@ def filter_claude_mpm_args(claude_args):
33
33
  flags and will error if they're passed through.
34
34
 
35
35
  DESIGN DECISION: We maintain a list of known claude-mpm flags to filter out,
36
- ensuring only genuine Claude CLI arguments are passed through.
36
+ ensuring only genuine Claude CLI arguments are passed through. We also remove
37
+ the '--' separator that argparse uses, as it's not needed by Claude CLI.
37
38
 
38
39
  Args:
39
40
  claude_args: List of arguments captured by argparse.REMAINDER
@@ -83,6 +84,11 @@ def filter_claude_mpm_args(claude_args):
83
84
  while i < len(claude_args):
84
85
  arg = claude_args[i]
85
86
 
87
+ # Skip the '--' separator used by argparse - Claude doesn't need it
88
+ if arg == "--":
89
+ i += 1
90
+ continue
91
+
86
92
  # Check if this is a claude-mpm flag
87
93
  if arg in mpm_flags:
88
94
  # Skip this flag
@@ -379,16 +385,37 @@ def run_session(args):
379
385
  # Create simple runner
380
386
  enable_tickets = not args.no_tickets
381
387
  raw_claude_args = getattr(args, "claude_args", []) or []
388
+
389
+ # Add --resume to claude_args if the flag is set
390
+ resume_flag_present = getattr(args, "resume", False)
391
+ if resume_flag_present:
392
+ logger.info("📌 --resume flag detected in args")
393
+ if "--resume" not in raw_claude_args:
394
+ raw_claude_args = ["--resume"] + raw_claude_args
395
+ logger.info("✅ Added --resume to claude_args")
396
+ else:
397
+ logger.info("ℹ️ --resume already in claude_args")
398
+
382
399
  # Filter out claude-mpm specific flags before passing to Claude CLI
400
+ logger.debug(f"Pre-filter claude_args: {raw_claude_args}")
383
401
  claude_args = filter_claude_mpm_args(raw_claude_args)
384
402
  monitor_mode = getattr(args, "monitor", False)
385
403
 
386
- # Debug logging for argument filtering
404
+ # Enhanced debug logging for argument filtering
387
405
  if raw_claude_args != claude_args:
388
- logger.debug(
389
- f"Filtered claude-mpm args: {set(raw_claude_args) - set(claude_args)}"
390
- )
391
- logger.debug(f"Passing to Claude CLI: {claude_args}")
406
+ filtered_out = list(set(raw_claude_args) - set(claude_args))
407
+ logger.debug(f"Filtered out MPM-specific args: {filtered_out}")
408
+
409
+ logger.info(f"Final claude_args being passed: {claude_args}")
410
+
411
+ # Explicit verification of --resume flag
412
+ if resume_flag_present:
413
+ if "--resume" in claude_args:
414
+ logger.info("✅ CONFIRMED: --resume flag will be passed to Claude CLI")
415
+ else:
416
+ logger.error("❌ WARNING: --resume flag was filtered out! This is a bug!")
417
+ logger.error(f" Original args: {raw_claude_args}")
418
+ logger.error(f" Filtered args: {claude_args}")
392
419
 
393
420
  # Use the specified launch method (default: exec)
394
421
  launch_method = getattr(args, "launch_method", "exec")
@@ -187,6 +187,11 @@ def add_top_level_run_arguments(parser: argparse.ArgumentParser) -> None:
187
187
  const="last",
188
188
  help="Resume an MPM session (last session if no ID specified, or specific session ID)",
189
189
  )
190
+ run_group.add_argument(
191
+ "--resume",
192
+ action="store_true",
193
+ help="Pass --resume flag to Claude Desktop to resume the last conversation",
194
+ )
190
195
  run_group.add_argument(
191
196
  "--force",
192
197
  action="store_true",
@@ -75,6 +75,11 @@ def add_run_arguments(parser: argparse.ArgumentParser) -> None:
75
75
  const="last",
76
76
  help="Resume an MPM session (last session if no ID specified, or specific session ID)",
77
77
  )
78
+ run_group.add_argument(
79
+ "--resume",
80
+ action="store_true",
81
+ help="Pass --resume flag to Claude Desktop to resume the last conversation",
82
+ )
78
83
 
79
84
  # Dependency checking options
80
85
  dep_group = parser.add_argument_group("dependency options")
claude_mpm/cli/utils.py CHANGED
@@ -132,11 +132,24 @@ def list_agent_versions_at_startup() -> None:
132
132
 
133
133
  WHY: Users want to see what agents are available when they start a session.
134
134
  This provides immediate feedback about the deployed agent environment.
135
+
136
+ DESIGN DECISION: We suppress INFO logging during this call to avoid duplicate
137
+ initialization messages since the deployment service will be initialized again
138
+ later in the ClaudeRunner.
135
139
  """
136
- agent_versions = get_agent_versions_display()
137
- if agent_versions:
138
- print(agent_versions)
139
- print() # Extra newline after the display
140
+ # Temporarily suppress INFO level logging to avoid duplicate initialization messages
141
+ import logging
142
+ original_level = logging.getLogger("claude_mpm").level
143
+ logging.getLogger("claude_mpm").setLevel(logging.WARNING)
144
+
145
+ try:
146
+ agent_versions = get_agent_versions_display()
147
+ if agent_versions:
148
+ print(agent_versions)
149
+ print() # Extra newline after the display
150
+ finally:
151
+ # Restore original logging level
152
+ logging.getLogger("claude_mpm").setLevel(original_level)
140
153
 
141
154
 
142
155
  def setup_logging(args) -> object:
@@ -159,7 +159,7 @@ class BaseService(LoggerMixin, ABC):
159
159
 
160
160
  # Only log if not in quiet mode
161
161
  if not os.environ.get("CLAUDE_PM_QUIET_MODE", "").lower() == "true":
162
- self.logger.info(f"Initialized {self.name} service")
162
+ self.logger.debug(f"Initialized {self.name} service")
163
163
 
164
164
  def _init_enhanced_features(self):
165
165
  """Initialize enhanced features when enabled."""
claude_mpm/core/config.py CHANGED
@@ -24,6 +24,9 @@ class Config:
24
24
  """
25
25
  Configuration manager for Claude PM services.
26
26
 
27
+ Implements singleton pattern to ensure configuration is loaded only once
28
+ and shared across all services.
29
+
27
30
  Supports loading from:
28
31
  - Python dictionaries
29
32
  - JSON files
@@ -31,6 +34,23 @@ class Config:
31
34
  - Environment variables
32
35
  """
33
36
 
37
+ _instance = None
38
+ _initialized = False
39
+ _success_logged = False # Class-level flag to track if success message was already logged
40
+
41
+ def __new__(cls, *args, **kwargs):
42
+ """Implement singleton pattern to ensure single configuration instance.
43
+
44
+ WHY: Configuration was being loaded 11 times during startup, once for each service.
45
+ This singleton pattern ensures configuration is loaded only once and reused.
46
+ """
47
+ if cls._instance is None:
48
+ cls._instance = super().__new__(cls)
49
+ logger.info("Creating new Config singleton instance")
50
+ else:
51
+ logger.debug("Reusing existing Config singleton instance")
52
+ return cls._instance
53
+
34
54
  def __init__(
35
55
  self,
36
56
  config: Optional[Dict[str, Any]] = None,
@@ -45,6 +65,21 @@ class Config:
45
65
  config_file: Path to configuration file (JSON or YAML)
46
66
  env_prefix: Prefix for environment variables
47
67
  """
68
+ # Skip initialization if already done (singleton pattern)
69
+ if Config._initialized:
70
+ logger.debug("Config already initialized, skipping re-initialization")
71
+ # If someone tries to load a different config file after initialization,
72
+ # log a debug message but don't reload
73
+ if config_file and str(config_file) != getattr(self, '_loaded_from', None):
74
+ logger.debug(
75
+ f"Ignoring config_file parameter '{config_file}' - "
76
+ f"configuration already loaded from '{getattr(self, '_loaded_from', 'defaults')}'"
77
+ )
78
+ return
79
+
80
+ Config._initialized = True
81
+ logger.info("Initializing Config singleton for the first time")
82
+
48
83
  self._config: Dict[str, Any] = {}
49
84
  self._env_prefix = env_prefix
50
85
  self._config_mgr = ConfigurationManager(cache_enabled=True)
@@ -55,22 +90,24 @@ class Config:
55
90
 
56
91
  # Track where configuration was loaded from
57
92
  self._loaded_from = None
93
+ # Track the actual file we loaded from to prevent re-loading
94
+ self._actual_loaded_file = None
58
95
 
59
96
  # Load from file if provided
60
97
  if config_file:
61
- self.load_file(config_file)
98
+ self.load_file(config_file, is_initial_load=True)
62
99
  self._loaded_from = str(config_file)
63
100
  else:
64
101
  # Try to load from standard location: .claude-mpm/configuration.yaml
65
102
  default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
66
103
  if default_config.exists():
67
- self.load_file(default_config)
104
+ self.load_file(default_config, is_initial_load=True)
68
105
  self._loaded_from = str(default_config)
69
106
  else:
70
107
  # Also try .yml extension
71
108
  alt_config = Path.cwd() / ".claude-mpm" / "configuration.yml"
72
109
  if alt_config.exists():
73
- self.load_file(alt_config)
110
+ self.load_file(alt_config, is_initial_load=True)
74
111
  self._loaded_from = str(alt_config)
75
112
 
76
113
  # Load from environment variables (new and legacy prefixes)
@@ -80,13 +117,22 @@ class Config:
80
117
  # Apply defaults
81
118
  self._apply_defaults()
82
119
 
83
- def load_file(self, file_path: Union[str, Path]) -> None:
120
+ def load_file(self, file_path: Union[str, Path], is_initial_load: bool = True) -> None:
84
121
  """Load configuration from file with enhanced error handling.
85
122
 
86
123
  WHY: Configuration loading failures can cause silent issues. We need
87
124
  to provide clear, actionable error messages to help users fix problems.
125
+
126
+ Args:
127
+ file_path: Path to the configuration file
128
+ is_initial_load: Whether this is the initial configuration load (for logging control)
88
129
  """
89
130
  file_path = Path(file_path)
131
+
132
+ # Check if we've already loaded from this exact file to prevent duplicate messages
133
+ if hasattr(self, '_actual_loaded_file') and self._actual_loaded_file == str(file_path):
134
+ logger.debug(f"Configuration already loaded from {file_path}, skipping reload")
135
+ return
90
136
 
91
137
  if not file_path.exists():
92
138
  logger.warning(f"Configuration file not found: {file_path}")
@@ -113,7 +159,14 @@ class Config:
113
159
  file_config = self._config_mgr.load_auto(file_path)
114
160
  if file_config:
115
161
  self._config = self._config_mgr.merge_configs(self._config, file_config)
116
- logger.info(f"✓ Successfully loaded configuration from {file_path}")
162
+ # Track that we've successfully loaded from this file
163
+ self._actual_loaded_file = str(file_path)
164
+ # Only log success message once using class-level flag to avoid duplicate messages
165
+ if is_initial_load and not Config._success_logged:
166
+ logger.info(f"✓ Successfully loaded configuration from {file_path}")
167
+ Config._success_logged = True
168
+ else:
169
+ logger.debug(f"Configuration reloaded from {file_path}")
117
170
 
118
171
  # Log important configuration values for debugging
119
172
  if logger.isEnabledFor(logging.DEBUG):
@@ -789,3 +842,15 @@ class Config:
789
842
  def __repr__(self) -> str:
790
843
  """String representation of configuration."""
791
844
  return f"<Config({len(self._config)} keys)>"
845
+
846
+ @classmethod
847
+ def reset_singleton(cls):
848
+ """Reset the singleton instance (mainly for testing purposes).
849
+
850
+ WHY: During testing, we may need to reset the singleton to test different
851
+ configurations. This method allows controlled reset of the singleton state.
852
+ """
853
+ cls._instance = None
854
+ cls._initialized = False
855
+ cls._success_logged = False
856
+ logger.debug("Config singleton reset")