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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__main__.py +4 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
- claude_mpm/agents/OUTPUT_STYLE.md +84 -0
- claude_mpm/agents/templates/qa.json +24 -12
- claude_mpm/cli/__init__.py +85 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/commands/mcp_install_commands.py +62 -5
- claude_mpm/cli/commands/mcp_server_commands.py +60 -79
- claude_mpm/cli/commands/memory.py +32 -5
- claude_mpm/cli/commands/run.py +33 -6
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/run_parser.py +5 -0
- claude_mpm/cli/utils.py +17 -4
- claude_mpm/core/base_service.py +1 -1
- claude_mpm/core/config.py +70 -5
- claude_mpm/core/framework_loader.py +342 -31
- claude_mpm/core/interactive_session.py +55 -1
- claude_mpm/core/oneshot_session.py +7 -1
- claude_mpm/core/output_style_manager.py +468 -0
- claude_mpm/core/unified_paths.py +190 -21
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
- claude_mpm/init.py +1 -0
- claude_mpm/scripts/mcp_server.py +68 -0
- claude_mpm/scripts/mcp_wrapper.py +39 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +151 -7
- claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
- claude_mpm/services/agents/memory/__init__.py +0 -2
- claude_mpm/services/agents/memory/agent_memory_manager.py +737 -43
- claude_mpm/services/agents/memory/content_manager.py +144 -14
- claude_mpm/services/agents/memory/template_generator.py +7 -354
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +312 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +315 -0
- claude_mpm/services/mcp_gateway/main.py +7 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +184 -176
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +453 -0
- claude_mpm/services/subprocess_launcher_service.py +5 -0
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/RECORD +45 -38
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/entry_points.txt +1 -0
- claude_mpm/services/agents/memory/analyzer.py +0 -430
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
claude_mpm/cli/commands/run.py
CHANGED
|
@@ -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
|
-
#
|
|
404
|
+
# Enhanced debug logging for argument filtering
|
|
387
405
|
if raw_claude_args != claude_args:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
claude_mpm/core/base_service.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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")
|