claude-mpm 4.0.20__py3-none-any.whl → 4.0.23__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/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +74 -0
- claude_mpm/agents/WORKFLOW.md +308 -4
- claude_mpm/agents/agents_metadata.py +52 -0
- claude_mpm/agents/base_agent_loader.py +75 -19
- claude_mpm/agents/templates/__init__.py +4 -0
- claude_mpm/agents/templates/api_qa.json +206 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +2 -2
- claude_mpm/agents/templates/documentation.json +36 -9
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/ops.json +2 -2
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +65 -43
- claude_mpm/agents/templates/research.json +24 -16
- claude_mpm/agents/templates/security.json +2 -2
- claude_mpm/agents/templates/ticketing.json +18 -5
- claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
- claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
- claude_mpm/agents/templates/version_control.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/commands/mcp_command_router.py +87 -1
- claude_mpm/cli/commands/mcp_install_commands.py +207 -26
- claude_mpm/cli/parsers/mcp_parser.py +23 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/base_service.py +7 -1
- claude_mpm/core/config.py +64 -39
- claude_mpm/core/framework_loader.py +100 -37
- claude_mpm/core/interactive_session.py +28 -17
- claude_mpm/scripts/socketio_daemon.py +67 -7
- claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +65 -3
- claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -203
- claude_mpm/services/memory_hook_service.py +62 -4
- claude_mpm/services/runner_configuration_service.py +5 -9
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/core.py +4 -0
- claude_mpm/services/socketio/server/main.py +23 -4
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/RECORD +46 -42
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/top_level.txt +0 -0
claude_mpm/core/config.py
CHANGED
|
@@ -8,6 +8,7 @@ and default values with proper validation and type conversion.
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import threading
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
14
|
|
|
@@ -37,16 +38,23 @@ class Config:
|
|
|
37
38
|
_instance = None
|
|
38
39
|
_initialized = False
|
|
39
40
|
_success_logged = False # Class-level flag to track if success message was already logged
|
|
41
|
+
_lock = threading.Lock() # Thread safety for singleton initialization
|
|
40
42
|
|
|
41
43
|
def __new__(cls, *args, **kwargs):
|
|
42
44
|
"""Implement singleton pattern to ensure single configuration instance.
|
|
43
45
|
|
|
44
46
|
WHY: Configuration was being loaded 11 times during startup, once for each service.
|
|
45
47
|
This singleton pattern ensures configuration is loaded only once and reused.
|
|
48
|
+
Thread-safe implementation prevents race conditions during concurrent initialization.
|
|
46
49
|
"""
|
|
47
50
|
if cls._instance is None:
|
|
48
|
-
cls.
|
|
49
|
-
|
|
51
|
+
with cls._lock:
|
|
52
|
+
# Double-check locking pattern for thread safety
|
|
53
|
+
if cls._instance is None:
|
|
54
|
+
cls._instance = super().__new__(cls)
|
|
55
|
+
logger.info("Creating new Config singleton instance")
|
|
56
|
+
else:
|
|
57
|
+
logger.debug("Reusing existing Config singleton instance (concurrent init)")
|
|
50
58
|
else:
|
|
51
59
|
logger.debug("Reusing existing Config singleton instance")
|
|
52
60
|
return cls._instance
|
|
@@ -66,6 +74,7 @@ class Config:
|
|
|
66
74
|
env_prefix: Prefix for environment variables
|
|
67
75
|
"""
|
|
68
76
|
# Skip initialization if already done (singleton pattern)
|
|
77
|
+
# Use thread-safe check to prevent concurrent initialization
|
|
69
78
|
if Config._initialized:
|
|
70
79
|
logger.debug("Config already initialized, skipping re-initialization")
|
|
71
80
|
# If someone tries to load a different config file after initialization,
|
|
@@ -77,45 +86,52 @@ class Config:
|
|
|
77
86
|
)
|
|
78
87
|
return
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
# Thread-safe initialization - acquire lock for ENTIRE initialization process
|
|
90
|
+
with Config._lock:
|
|
91
|
+
# Double-check pattern - check again inside the lock
|
|
92
|
+
if Config._initialized:
|
|
93
|
+
logger.debug("Config already initialized (concurrent), skipping re-initialization")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
Config._initialized = True
|
|
97
|
+
logger.info("Initializing Config singleton for the first time")
|
|
98
|
+
|
|
99
|
+
# Initialize instance variables inside the lock to ensure thread safety
|
|
100
|
+
self._config: Dict[str, Any] = {}
|
|
101
|
+
self._env_prefix = env_prefix
|
|
102
|
+
self._config_mgr = ConfigurationManager(cache_enabled=True)
|
|
103
|
+
|
|
104
|
+
# Load base configuration
|
|
105
|
+
if config:
|
|
106
|
+
self._config.update(config)
|
|
107
|
+
|
|
108
|
+
# Track where configuration was loaded from
|
|
109
|
+
self._loaded_from = None
|
|
110
|
+
# Track the actual file we loaded from to prevent re-loading
|
|
111
|
+
self._actual_loaded_file = None
|
|
112
|
+
|
|
113
|
+
# Load from file if provided
|
|
114
|
+
# Note: Only ONE config file should be loaded, and success message shown only once
|
|
115
|
+
if config_file:
|
|
116
|
+
self.load_file(config_file, is_initial_load=True)
|
|
117
|
+
self._loaded_from = str(config_file)
|
|
106
118
|
else:
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
if
|
|
119
|
+
# Try to load from standard location: .claude-mpm/configuration.yaml
|
|
120
|
+
default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
|
|
121
|
+
if default_config.exists():
|
|
122
|
+
self.load_file(default_config, is_initial_load=True)
|
|
123
|
+
self._loaded_from = str(default_config)
|
|
124
|
+
elif (alt_config := Path.cwd() / ".claude-mpm" / "configuration.yml").exists():
|
|
125
|
+
# Also try .yml extension (using walrus operator for cleaner code)
|
|
110
126
|
self.load_file(alt_config, is_initial_load=True)
|
|
111
127
|
self._loaded_from = str(alt_config)
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
# Load from environment variables (new and legacy prefixes)
|
|
130
|
+
self._load_env_vars()
|
|
131
|
+
self._load_legacy_env_vars()
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
# Apply defaults
|
|
134
|
+
self._apply_defaults()
|
|
119
135
|
|
|
120
136
|
def load_file(self, file_path: Union[str, Path], is_initial_load: bool = True) -> None:
|
|
121
137
|
"""Load configuration from file with enhanced error handling.
|
|
@@ -161,11 +177,20 @@ class Config:
|
|
|
161
177
|
self._config = self._config_mgr.merge_configs(self._config, file_config)
|
|
162
178
|
# Track that we've successfully loaded from this file
|
|
163
179
|
self._actual_loaded_file = str(file_path)
|
|
180
|
+
|
|
164
181
|
# Only log success message once using class-level flag to avoid duplicate messages
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
Config._success_logged
|
|
182
|
+
# Check if we should log success message (thread-safe for reads after initialization)
|
|
183
|
+
if is_initial_load:
|
|
184
|
+
if not Config._success_logged:
|
|
185
|
+
# Set flag IMMEDIATELY before logging to prevent any possibility of duplicate
|
|
186
|
+
# messages. No lock needed here since we're already inside __init__ lock
|
|
187
|
+
Config._success_logged = True
|
|
188
|
+
logger.info(f"✓ Successfully loaded configuration from {file_path}")
|
|
189
|
+
else:
|
|
190
|
+
# Configuration already successfully loaded before, just debug log
|
|
191
|
+
logger.debug(f"Configuration already loaded, skipping success message for {file_path}")
|
|
168
192
|
else:
|
|
193
|
+
# Not initial load (shouldn't happen in normal flow, but handle gracefully)
|
|
169
194
|
logger.debug(f"Configuration reloaded from {file_path}")
|
|
170
195
|
|
|
171
196
|
# Log important configuration values for debugging
|
|
@@ -489,9 +489,25 @@ class FrameworkLoader:
|
|
|
489
489
|
memory_size = len(memory_content.encode('utf-8'))
|
|
490
490
|
self.logger.debug(f"Aggregated {agent_name} memory: {memory_size:,} bytes")
|
|
491
491
|
|
|
492
|
-
# Log summary
|
|
492
|
+
# Log detailed summary
|
|
493
493
|
if loaded_count > 0 or skipped_count > 0:
|
|
494
|
-
|
|
494
|
+
# Count unique agents with memories
|
|
495
|
+
agent_count = len(agent_memories_dict) if agent_memories_dict else 0
|
|
496
|
+
pm_loaded = bool(content.get("actual_memories"))
|
|
497
|
+
|
|
498
|
+
summary_parts = []
|
|
499
|
+
if pm_loaded:
|
|
500
|
+
summary_parts.append("PM memory loaded")
|
|
501
|
+
if agent_count > 0:
|
|
502
|
+
summary_parts.append(f"{agent_count} agent memories loaded")
|
|
503
|
+
if skipped_count > 0:
|
|
504
|
+
summary_parts.append(f"{skipped_count} non-deployed agent memories skipped")
|
|
505
|
+
|
|
506
|
+
self.logger.info(f"Memory loading complete: {' | '.join(summary_parts)}")
|
|
507
|
+
|
|
508
|
+
# Log deployed agents for reference
|
|
509
|
+
if len(deployed_agents) > 0:
|
|
510
|
+
self.logger.debug(f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}")
|
|
495
511
|
|
|
496
512
|
def _load_memories_from_directory(
|
|
497
513
|
self,
|
|
@@ -544,36 +560,36 @@ class FrameworkLoader:
|
|
|
544
560
|
self.logger.info(f"Loaded {source} PM memory: {pm_memory_path} ({memory_size:,} bytes)")
|
|
545
561
|
loaded_count += 1
|
|
546
562
|
|
|
547
|
-
#
|
|
548
|
-
for
|
|
549
|
-
|
|
550
|
-
|
|
563
|
+
# First, migrate any old format memory files to new format
|
|
564
|
+
# This handles backward compatibility for existing installations
|
|
565
|
+
for old_file in memories_dir.glob("*.md"):
|
|
566
|
+
# Skip files already in correct format and special files
|
|
567
|
+
if old_file.name.endswith("_memories.md") or old_file.name in ["PM.md", "README.md"]:
|
|
551
568
|
continue
|
|
552
569
|
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if agent_name.endswith("_agent"):
|
|
559
|
-
# Old format: {agent_name}_agent.md
|
|
560
|
-
agent_name = agent_name[:-6] # Remove "_agent" suffix
|
|
570
|
+
# Determine new name based on old format
|
|
571
|
+
if old_file.stem.endswith("_agent"):
|
|
572
|
+
# Old format: {agent_name}_agent.md -> {agent_name}_memories.md
|
|
573
|
+
agent_name = old_file.stem[:-6] # Remove "_agent" suffix
|
|
561
574
|
new_path = memories_dir / f"{agent_name}_memories.md"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
# agent_name already set from stem
|
|
575
|
+
if not new_path.exists():
|
|
576
|
+
self._migrate_memory_file(old_file, new_path)
|
|
577
|
+
else:
|
|
578
|
+
# Intermediate format: {agent_name}.md -> {agent_name}_memories.md
|
|
579
|
+
agent_name = old_file.stem
|
|
568
580
|
new_path = memories_dir / f"{agent_name}_memories.md"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
581
|
+
if not new_path.exists():
|
|
582
|
+
self._migrate_memory_file(old_file, new_path)
|
|
583
|
+
|
|
584
|
+
# Load agent memories (only for deployed agents)
|
|
585
|
+
# Only process *_memories.md files to avoid README.md and other docs
|
|
586
|
+
for memory_file in memories_dir.glob("*_memories.md"):
|
|
587
|
+
# Skip PM_memories.md as we already handled it
|
|
588
|
+
if memory_file.name == "PM_memories.md":
|
|
572
589
|
continue
|
|
573
|
-
|
|
574
|
-
#
|
|
575
|
-
|
|
576
|
-
self._migrate_memory_file(memory_file, new_path)
|
|
590
|
+
|
|
591
|
+
# Extract agent name from file (remove "_memories" suffix)
|
|
592
|
+
agent_name = memory_file.stem[:-9] # Remove "_memories" suffix
|
|
577
593
|
|
|
578
594
|
# Check if agent is deployed
|
|
579
595
|
if agent_name in deployed_agents:
|
|
@@ -597,7 +613,17 @@ class FrameworkLoader:
|
|
|
597
613
|
self.logger.info(f"Loaded {source} memory for {agent_name}: {memory_file.name} ({memory_size:,} bytes)")
|
|
598
614
|
loaded_count += 1
|
|
599
615
|
else:
|
|
600
|
-
|
|
616
|
+
# Provide more detailed logging about why the memory was skipped
|
|
617
|
+
self.logger.info(f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed)")
|
|
618
|
+
# Also log a debug message with available agents for diagnostics
|
|
619
|
+
if agent_name.replace('_', '-') in deployed_agents or agent_name.replace('-', '_') in deployed_agents:
|
|
620
|
+
# Detect naming mismatches
|
|
621
|
+
alt_name = agent_name.replace('_', '-') if '_' in agent_name else agent_name.replace('-', '_')
|
|
622
|
+
if alt_name in deployed_agents:
|
|
623
|
+
self.logger.warning(
|
|
624
|
+
f"Naming mismatch detected: Memory file uses '{agent_name}' but deployed agent is '{alt_name}'. "
|
|
625
|
+
f"Consider renaming {memory_file.name} to {alt_name}_memories.md"
|
|
626
|
+
)
|
|
601
627
|
skipped_count += 1
|
|
602
628
|
|
|
603
629
|
# After loading all memories for this directory, aggregate agent memories
|
|
@@ -614,9 +640,10 @@ class FrameworkLoader:
|
|
|
614
640
|
Aggregate multiple memory entries into a single memory string.
|
|
615
641
|
|
|
616
642
|
Strategy:
|
|
617
|
-
-
|
|
618
|
-
-
|
|
619
|
-
-
|
|
643
|
+
- Support both sectioned and non-sectioned memories
|
|
644
|
+
- Preserve all bullet-point items (lines starting with -)
|
|
645
|
+
- Merge sections when present, with project-level taking precedence
|
|
646
|
+
- Remove exact duplicates within sections and unsectioned items
|
|
620
647
|
- Preserve unique entries from both sources
|
|
621
648
|
|
|
622
649
|
Args:
|
|
@@ -632,15 +659,16 @@ class FrameworkLoader:
|
|
|
632
659
|
if len(memory_entries) == 1:
|
|
633
660
|
return memory_entries[0]["content"]
|
|
634
661
|
|
|
635
|
-
# Parse all memories into sections
|
|
662
|
+
# Parse all memories into sections and unsectioned items
|
|
636
663
|
all_sections = {}
|
|
664
|
+
unsectioned_items = {} # Items without a section header
|
|
637
665
|
metadata_lines = []
|
|
638
666
|
|
|
639
667
|
for entry in memory_entries:
|
|
640
668
|
content = entry["content"]
|
|
641
669
|
source = entry["source"]
|
|
642
670
|
|
|
643
|
-
# Parse content into sections
|
|
671
|
+
# Parse content into sections and unsectioned items
|
|
644
672
|
current_section = None
|
|
645
673
|
current_items = []
|
|
646
674
|
|
|
@@ -664,11 +692,25 @@ class FrameworkLoader:
|
|
|
664
692
|
# Start new section
|
|
665
693
|
current_section = line
|
|
666
694
|
current_items = []
|
|
667
|
-
# Check for content lines (
|
|
668
|
-
elif line.strip()
|
|
669
|
-
|
|
695
|
+
# Check for content lines (including unsectioned bullet points)
|
|
696
|
+
elif line.strip():
|
|
697
|
+
# If it's a bullet point or regular content
|
|
698
|
+
if current_section:
|
|
699
|
+
# Add to current section
|
|
700
|
+
current_items.append(line)
|
|
701
|
+
elif line.strip().startswith('-'):
|
|
702
|
+
# It's an unsectioned bullet point - preserve it
|
|
703
|
+
# Use content as key to detect duplicates
|
|
704
|
+
# Project source overrides user source
|
|
705
|
+
if line not in unsectioned_items or source == "project":
|
|
706
|
+
unsectioned_items[line] = source
|
|
707
|
+
# Skip other non-bullet unsectioned content (like headers)
|
|
708
|
+
elif not line.strip().startswith('#'):
|
|
709
|
+
# Include non-header orphaned content in unsectioned items
|
|
710
|
+
if line not in unsectioned_items or source == "project":
|
|
711
|
+
unsectioned_items[line] = source
|
|
670
712
|
|
|
671
|
-
# Save last section
|
|
713
|
+
# Save last section if exists
|
|
672
714
|
if current_section and current_items:
|
|
673
715
|
if current_section not in all_sections:
|
|
674
716
|
all_sections[current_section] = {}
|
|
@@ -691,6 +733,13 @@ class FrameworkLoader:
|
|
|
691
733
|
lines.append("*This memory combines user-level and project-level memories.*")
|
|
692
734
|
lines.append("")
|
|
693
735
|
|
|
736
|
+
# Add unsectioned items first (if any)
|
|
737
|
+
if unsectioned_items:
|
|
738
|
+
# Sort items to ensure consistent output
|
|
739
|
+
for item in sorted(unsectioned_items.keys()):
|
|
740
|
+
lines.append(item)
|
|
741
|
+
lines.append("") # Empty line after unsectioned items
|
|
742
|
+
|
|
694
743
|
# Add sections
|
|
695
744
|
for section_header in sorted(all_sections.keys()):
|
|
696
745
|
lines.append(section_header)
|
|
@@ -1075,6 +1124,20 @@ class FrameworkLoader:
|
|
|
1075
1124
|
instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
|
|
1076
1125
|
instructions += self.framework_content["actual_memories"]
|
|
1077
1126
|
instructions += "\n"
|
|
1127
|
+
|
|
1128
|
+
# Add agent memories if available
|
|
1129
|
+
if self.framework_content.get("agent_memories"):
|
|
1130
|
+
agent_memories = self.framework_content["agent_memories"]
|
|
1131
|
+
if agent_memories:
|
|
1132
|
+
instructions += "\n\n## Agent Memories\n\n"
|
|
1133
|
+
instructions += "**The following are accumulated memories from specialized agents:**\n\n"
|
|
1134
|
+
|
|
1135
|
+
for agent_name in sorted(agent_memories.keys()):
|
|
1136
|
+
memory_content = agent_memories[agent_name]
|
|
1137
|
+
if memory_content:
|
|
1138
|
+
instructions += f"### {agent_name.replace('_', ' ').title()} Agent Memory\n\n"
|
|
1139
|
+
instructions += memory_content
|
|
1140
|
+
instructions += "\n\n"
|
|
1078
1141
|
|
|
1079
1142
|
# Add dynamic agent capabilities section
|
|
1080
1143
|
instructions += self._generate_agent_capabilities_section()
|
|
@@ -344,25 +344,36 @@ class InteractiveSession:
|
|
|
344
344
|
|
|
345
345
|
def _build_claude_command(self) -> list:
|
|
346
346
|
"""Build the Claude command with all necessary arguments."""
|
|
347
|
-
|
|
347
|
+
# Check if --resume flag is present
|
|
348
|
+
has_resume = self.runner.claude_args and "--resume" in self.runner.claude_args
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
350
|
+
if has_resume:
|
|
351
|
+
# When resuming, use minimal command to avoid interfering with conversation selection
|
|
352
|
+
self.logger.info("🔄 Resume mode detected - using minimal Claude command to preserve conversation selection")
|
|
353
|
+
cmd = ["claude"]
|
|
354
|
+
|
|
355
|
+
# Add only the claude_args (which includes --resume)
|
|
356
|
+
if self.runner.claude_args:
|
|
357
|
+
cmd.extend(self.runner.claude_args)
|
|
358
|
+
self.logger.info(f"Resume command: {cmd}")
|
|
359
|
+
|
|
360
|
+
return cmd
|
|
361
|
+
else:
|
|
362
|
+
# Normal mode - full command with all claude-mpm enhancements
|
|
363
|
+
cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
|
|
364
|
+
|
|
365
|
+
# Add custom arguments
|
|
366
|
+
if self.runner.claude_args:
|
|
367
|
+
# Enhanced debug logging for --resume flag verification
|
|
368
|
+
self.logger.debug(f"Raw claude_args received: {self.runner.claude_args}")
|
|
369
|
+
cmd.extend(self.runner.claude_args)
|
|
370
|
+
|
|
371
|
+
# Add system instructions
|
|
372
|
+
from claude_mpm.core.claude_runner import create_simple_context
|
|
362
373
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
system_prompt = self.runner._create_system_prompt()
|
|
375
|
+
if system_prompt and system_prompt != create_simple_context():
|
|
376
|
+
cmd.extend(["--append-system-prompt", system_prompt])
|
|
366
377
|
|
|
367
378
|
# Final command verification
|
|
368
379
|
# self.logger.info(f"Final Claude command built: {' '.join(cmd)}")
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
1
|
#!/usr/bin/env python3
|
|
4
2
|
"""
|
|
5
3
|
Pure Python daemon management for Socket.IO server.
|
|
@@ -12,6 +10,64 @@ import signal
|
|
|
12
10
|
import subprocess
|
|
13
11
|
import sys
|
|
14
12
|
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Detect and use virtual environment Python if available
|
|
16
|
+
def get_python_executable():
|
|
17
|
+
"""
|
|
18
|
+
Get the appropriate Python executable, preferring virtual environment.
|
|
19
|
+
|
|
20
|
+
WHY: The daemon must use the same Python environment as the parent process
|
|
21
|
+
to ensure all dependencies are available. System Python won't have the
|
|
22
|
+
required packages installed.
|
|
23
|
+
"""
|
|
24
|
+
# First, check if we're already in a virtual environment
|
|
25
|
+
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
|
|
26
|
+
# We're in a virtual environment, use its Python
|
|
27
|
+
return sys.executable
|
|
28
|
+
|
|
29
|
+
# Check for common virtual environment indicators
|
|
30
|
+
# 1. VIRTUAL_ENV environment variable (most common)
|
|
31
|
+
venv_path = os.environ.get('VIRTUAL_ENV')
|
|
32
|
+
if venv_path:
|
|
33
|
+
venv_python = Path(venv_path) / 'bin' / 'python'
|
|
34
|
+
if venv_python.exists():
|
|
35
|
+
return str(venv_python)
|
|
36
|
+
|
|
37
|
+
# 2. Check if current executable is in a venv directory structure
|
|
38
|
+
exe_path = Path(sys.executable).resolve()
|
|
39
|
+
for parent in exe_path.parents:
|
|
40
|
+
# Check for common venv directory names
|
|
41
|
+
if parent.name in ('venv', '.venv', 'env', '.env'):
|
|
42
|
+
# This looks like a virtual environment
|
|
43
|
+
return sys.executable
|
|
44
|
+
|
|
45
|
+
# Check for typical venv structure (bin/python or Scripts/python.exe)
|
|
46
|
+
if parent.name == 'bin' and (parent.parent / 'pyvenv.cfg').exists():
|
|
47
|
+
return sys.executable
|
|
48
|
+
if parent.name == 'Scripts' and (parent.parent / 'pyvenv.cfg').exists():
|
|
49
|
+
return sys.executable
|
|
50
|
+
|
|
51
|
+
# 3. Try to detect project-specific venv
|
|
52
|
+
# Look for venv in the project root (going up from script location)
|
|
53
|
+
script_path = Path(__file__).resolve()
|
|
54
|
+
for parent in script_path.parents:
|
|
55
|
+
# Stop at src or when we've gone too far up
|
|
56
|
+
if parent.name == 'src' or not (parent / 'src').exists():
|
|
57
|
+
# Check for venv directories
|
|
58
|
+
for venv_name in ('venv', '.venv', 'env', '.env'):
|
|
59
|
+
venv_dir = parent / venv_name
|
|
60
|
+
if venv_dir.exists():
|
|
61
|
+
venv_python = venv_dir / 'bin' / 'python'
|
|
62
|
+
if venv_python.exists():
|
|
63
|
+
return str(venv_python)
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# Fall back to current Python executable
|
|
67
|
+
return sys.executable
|
|
68
|
+
|
|
69
|
+
# Store the detected Python executable for daemon usage
|
|
70
|
+
PYTHON_EXECUTABLE = get_python_executable()
|
|
15
71
|
|
|
16
72
|
import psutil
|
|
17
73
|
|
|
@@ -145,11 +201,12 @@ def start_server():
|
|
|
145
201
|
|
|
146
202
|
ensure_dirs()
|
|
147
203
|
|
|
148
|
-
# Fork to create daemon
|
|
204
|
+
# Fork to create daemon using the correct Python environment
|
|
149
205
|
pid = os.fork()
|
|
150
206
|
if pid > 0:
|
|
151
207
|
# Parent process
|
|
152
208
|
print(f"Starting Socket.IO server on port {selected_port} (PID: {pid})...")
|
|
209
|
+
print(f"Using Python: {PYTHON_EXECUTABLE}")
|
|
153
210
|
|
|
154
211
|
# Register the instance
|
|
155
212
|
instance_id = port_manager.register_instance(selected_port, pid)
|
|
@@ -179,10 +236,13 @@ def start_server():
|
|
|
179
236
|
os.dup2(log.fileno(), sys.stdout.fileno())
|
|
180
237
|
os.dup2(log.fileno(), sys.stderr.fileno())
|
|
181
238
|
|
|
182
|
-
#
|
|
239
|
+
# Log environment information for debugging
|
|
183
240
|
print(
|
|
184
241
|
f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting Socket.IO server on port {selected_port}..."
|
|
185
242
|
)
|
|
243
|
+
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python executable: {sys.executable}")
|
|
244
|
+
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python version: {sys.version}")
|
|
245
|
+
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python path: {sys.path[:3]}...") # Show first 3 entries
|
|
186
246
|
server = SocketIOServer(host="localhost", port=selected_port)
|
|
187
247
|
|
|
188
248
|
# Handle signals
|
|
@@ -384,12 +444,12 @@ def main():
|
|
|
384
444
|
|
|
385
445
|
|
|
386
446
|
if __name__ == "__main__":
|
|
387
|
-
# Install psutil if not available
|
|
447
|
+
# Install psutil if not available (using correct Python)
|
|
388
448
|
try:
|
|
389
449
|
import psutil
|
|
390
450
|
except ImportError:
|
|
391
|
-
print("Installing psutil...")
|
|
392
|
-
subprocess.check_call([
|
|
451
|
+
print(f"Installing psutil using {PYTHON_EXECUTABLE}...")
|
|
452
|
+
subprocess.check_call([PYTHON_EXECUTABLE, "-m", "pip", "install", "psutil"])
|
|
393
453
|
import psutil
|
|
394
454
|
|
|
395
455
|
main()
|