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.
Files changed (46) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/INSTRUCTIONS.md +74 -0
  4. claude_mpm/agents/WORKFLOW.md +308 -4
  5. claude_mpm/agents/agents_metadata.py +52 -0
  6. claude_mpm/agents/base_agent_loader.py +75 -19
  7. claude_mpm/agents/templates/__init__.py +4 -0
  8. claude_mpm/agents/templates/api_qa.json +206 -0
  9. claude_mpm/agents/templates/code_analyzer.json +2 -2
  10. claude_mpm/agents/templates/data_engineer.json +2 -2
  11. claude_mpm/agents/templates/documentation.json +36 -9
  12. claude_mpm/agents/templates/engineer.json +2 -2
  13. claude_mpm/agents/templates/ops.json +2 -2
  14. claude_mpm/agents/templates/qa.json +2 -2
  15. claude_mpm/agents/templates/refactoring_engineer.json +65 -43
  16. claude_mpm/agents/templates/research.json +24 -16
  17. claude_mpm/agents/templates/security.json +2 -2
  18. claude_mpm/agents/templates/ticketing.json +18 -5
  19. claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
  20. claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
  21. claude_mpm/agents/templates/version_control.json +2 -2
  22. claude_mpm/agents/templates/web_ui.json +2 -2
  23. claude_mpm/cli/commands/mcp_command_router.py +87 -1
  24. claude_mpm/cli/commands/mcp_install_commands.py +207 -26
  25. claude_mpm/cli/parsers/mcp_parser.py +23 -0
  26. claude_mpm/constants.py +1 -0
  27. claude_mpm/core/base_service.py +7 -1
  28. claude_mpm/core/config.py +64 -39
  29. claude_mpm/core/framework_loader.py +100 -37
  30. claude_mpm/core/interactive_session.py +28 -17
  31. claude_mpm/scripts/socketio_daemon.py +67 -7
  32. claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
  33. claude_mpm/services/agents/deployment/agent_deployment.py +65 -3
  34. claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
  35. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -203
  36. claude_mpm/services/memory_hook_service.py +62 -4
  37. claude_mpm/services/runner_configuration_service.py +5 -9
  38. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  39. claude_mpm/services/socketio/server/core.py +4 -0
  40. claude_mpm/services/socketio/server/main.py +23 -4
  41. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/METADATA +1 -1
  42. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/RECORD +46 -42
  43. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/WHEEL +0 -0
  44. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.23.dist-info}/licenses/LICENSE +0 -0
  46. {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._instance = super().__new__(cls)
49
- logger.info("Creating new Config singleton instance")
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
- Config._initialized = True
81
- logger.info("Initializing Config singleton for the first time")
82
-
83
- self._config: Dict[str, Any] = {}
84
- self._env_prefix = env_prefix
85
- self._config_mgr = ConfigurationManager(cache_enabled=True)
86
-
87
- # Load base configuration
88
- if config:
89
- self._config.update(config)
90
-
91
- # Track where configuration was loaded from
92
- self._loaded_from = None
93
- # Track the actual file we loaded from to prevent re-loading
94
- self._actual_loaded_file = None
95
-
96
- # Load from file if provided
97
- if config_file:
98
- self.load_file(config_file, is_initial_load=True)
99
- self._loaded_from = str(config_file)
100
- else:
101
- # Try to load from standard location: .claude-mpm/configuration.yaml
102
- default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
103
- if default_config.exists():
104
- self.load_file(default_config, is_initial_load=True)
105
- self._loaded_from = str(default_config)
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
- # Also try .yml extension
108
- alt_config = Path.cwd() / ".claude-mpm" / "configuration.yml"
109
- if alt_config.exists():
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
- # Load from environment variables (new and legacy prefixes)
114
- self._load_env_vars()
115
- self._load_legacy_env_vars()
129
+ # Load from environment variables (new and legacy prefixes)
130
+ self._load_env_vars()
131
+ self._load_legacy_env_vars()
116
132
 
117
- # Apply defaults
118
- self._apply_defaults()
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 is_initial_load and not Config._success_logged:
166
- logger.info(f"✓ Successfully loaded configuration from {file_path}")
167
- Config._success_logged = True
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
- self.logger.info(f"Memory loading complete: {loaded_count} memories loaded, {skipped_count} skipped")
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
- # Load agent memories (only for deployed agents)
548
- for memory_file in memories_dir.glob("*.md"):
549
- # Skip PM_memories.md and PM.md as we already handled them
550
- if memory_file.name in ["PM_memories.md", "PM.md"]:
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
- # Extract agent name from file
554
- # Support migration from old formats to new {agent_name}_memories.md format
555
- agent_name = memory_file.stem
556
- new_path = None
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
- elif agent_name.endswith("_memories"):
563
- # Already in new format: {agent_name}_memories.md
564
- agent_name = agent_name[:-9] # Remove "_memories" suffix
565
- elif memory_file.name != "README.md":
566
- # Intermediate format: {agent_name}.md
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
- # Skip README.md
571
- if memory_file.name == "README.md":
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
- # Migrate if needed
575
- if new_path and not new_path.exists():
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
- self.logger.info(f"Skipped {source} memory: {memory_file.name} (agent not deployed)")
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
- - Parse memories by sections
618
- - Merge sections, with project-level taking precedence
619
- - Remove exact duplicates within sections
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 (skip empty lines)
668
- elif line.strip() and current_section:
669
- current_items.append(line)
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
- cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
347
+ # Check if --resume flag is present
348
+ has_resume = self.runner.claude_args and "--resume" in self.runner.claude_args
348
349
 
349
- # Add custom arguments
350
- if self.runner.claude_args:
351
- # Enhanced debug logging for --resume flag verification
352
- self.logger.debug(f"Raw claude_args received: {self.runner.claude_args}")
353
-
354
- # Check explicitly for --resume flag
355
- has_resume = "--resume" in self.runner.claude_args
356
- self.logger.info(f"--resume flag present in claude_args: {has_resume}")
357
-
358
- cmd.extend(self.runner.claude_args)
359
-
360
- # Add system instructions
361
- from claude_mpm.core.claude_runner import create_simple_context
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
- system_prompt = self.runner._create_system_prompt()
364
- if system_prompt and system_prompt != create_simple_context():
365
- cmd.extend(["--append-system-prompt", system_prompt])
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
- # Start server
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([sys.executable, "-m", "pip", "install", "psutil"])
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()