claude-mpm 4.5.8__py3-none-any.whl → 4.5.12__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 (226) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +20 -5
  3. claude_mpm/agents/agent_loader.py +19 -2
  4. claude_mpm/agents/base_agent_loader.py +5 -5
  5. claude_mpm/agents/frontmatter_validator.py +4 -4
  6. claude_mpm/agents/templates/agent-manager.json +3 -3
  7. claude_mpm/agents/templates/agentic-coder-optimizer.json +3 -3
  8. claude_mpm/agents/templates/api_qa.json +1 -1
  9. claude_mpm/agents/templates/clerk-ops.json +3 -3
  10. claude_mpm/agents/templates/code_analyzer.json +3 -3
  11. claude_mpm/agents/templates/dart_engineer.json +294 -0
  12. claude_mpm/agents/templates/data_engineer.json +3 -3
  13. claude_mpm/agents/templates/documentation.json +2 -2
  14. claude_mpm/agents/templates/engineer.json +2 -2
  15. claude_mpm/agents/templates/gcp_ops_agent.json +2 -2
  16. claude_mpm/agents/templates/imagemagick.json +1 -1
  17. claude_mpm/agents/templates/local_ops_agent.json +319 -41
  18. claude_mpm/agents/templates/memory_manager.json +2 -2
  19. claude_mpm/agents/templates/nextjs_engineer.json +2 -2
  20. claude_mpm/agents/templates/ops.json +2 -2
  21. claude_mpm/agents/templates/php-engineer.json +1 -1
  22. claude_mpm/agents/templates/project_organizer.json +1 -1
  23. claude_mpm/agents/templates/prompt-engineer.json +6 -4
  24. claude_mpm/agents/templates/python_engineer.json +2 -2
  25. claude_mpm/agents/templates/qa.json +1 -1
  26. claude_mpm/agents/templates/react_engineer.json +3 -3
  27. claude_mpm/agents/templates/refactoring_engineer.json +3 -3
  28. claude_mpm/agents/templates/research.json +2 -2
  29. claude_mpm/agents/templates/security.json +2 -2
  30. claude_mpm/agents/templates/ticketing.json +2 -2
  31. claude_mpm/agents/templates/typescript_engineer.json +2 -2
  32. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  33. claude_mpm/agents/templates/version_control.json +2 -2
  34. claude_mpm/agents/templates/web_qa.json +6 -6
  35. claude_mpm/agents/templates/web_ui.json +3 -3
  36. claude_mpm/cli/__init__.py +49 -19
  37. claude_mpm/cli/commands/agent_manager.py +3 -3
  38. claude_mpm/cli/commands/agents.py +6 -6
  39. claude_mpm/cli/commands/aggregate.py +4 -4
  40. claude_mpm/cli/commands/analyze.py +2 -2
  41. claude_mpm/cli/commands/analyze_code.py +1 -1
  42. claude_mpm/cli/commands/cleanup.py +3 -3
  43. claude_mpm/cli/commands/config.py +2 -2
  44. claude_mpm/cli/commands/configure.py +605 -21
  45. claude_mpm/cli/commands/dashboard.py +1 -1
  46. claude_mpm/cli/commands/debug.py +3 -3
  47. claude_mpm/cli/commands/doctor.py +1 -1
  48. claude_mpm/cli/commands/mcp.py +7 -7
  49. claude_mpm/cli/commands/mcp_command_router.py +1 -1
  50. claude_mpm/cli/commands/mcp_config.py +2 -2
  51. claude_mpm/cli/commands/mcp_external_commands.py +2 -2
  52. claude_mpm/cli/commands/mcp_install_commands.py +3 -3
  53. claude_mpm/cli/commands/mcp_pipx_config.py +2 -2
  54. claude_mpm/cli/commands/mcp_setup_external.py +3 -3
  55. claude_mpm/cli/commands/monitor.py +1 -1
  56. claude_mpm/cli/commands/mpm_init_handler.py +1 -1
  57. claude_mpm/cli/interactive/agent_wizard.py +1 -1
  58. claude_mpm/cli/parsers/configure_parser.py +5 -0
  59. claude_mpm/cli/parsers/search_parser.py +1 -1
  60. claude_mpm/cli/shared/argument_patterns.py +2 -2
  61. claude_mpm/cli/shared/base_command.py +1 -1
  62. claude_mpm/cli/startup_logging.py +4 -4
  63. claude_mpm/config/experimental_features.py +4 -4
  64. claude_mpm/config/socketio_config.py +2 -2
  65. claude_mpm/core/__init__.py +53 -17
  66. claude_mpm/core/agent_session_manager.py +2 -2
  67. claude_mpm/core/api_validator.py +3 -3
  68. claude_mpm/core/base_service.py +10 -1
  69. claude_mpm/core/cache.py +2 -2
  70. claude_mpm/core/config.py +5 -5
  71. claude_mpm/core/config_aliases.py +4 -4
  72. claude_mpm/core/config_constants.py +1 -1
  73. claude_mpm/core/error_handler.py +1 -1
  74. claude_mpm/core/file_utils.py +5 -5
  75. claude_mpm/core/framework/formatters/capability_generator.py +5 -5
  76. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  77. claude_mpm/core/framework/processors/metadata_processor.py +1 -1
  78. claude_mpm/core/framework/processors/template_processor.py +3 -3
  79. claude_mpm/core/framework_loader.py +2 -2
  80. claude_mpm/core/log_manager.py +11 -4
  81. claude_mpm/core/logger.py +2 -2
  82. claude_mpm/core/optimized_startup.py +1 -1
  83. claude_mpm/core/output_style_manager.py +1 -1
  84. claude_mpm/core/service_registry.py +2 -2
  85. claude_mpm/core/session_manager.py +3 -3
  86. claude_mpm/core/shared/config_loader.py +1 -1
  87. claude_mpm/core/socketio_pool.py +2 -2
  88. claude_mpm/core/unified_agent_registry.py +2 -2
  89. claude_mpm/core/unified_config.py +6 -6
  90. claude_mpm/core/unified_paths.py +2 -2
  91. claude_mpm/dashboard/api/simple_directory.py +1 -1
  92. claude_mpm/generators/agent_profile_generator.py +1 -1
  93. claude_mpm/hooks/claude_hooks/event_handlers.py +2 -2
  94. claude_mpm/hooks/claude_hooks/installer.py +9 -9
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +16 -11
  96. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +16 -13
  97. claude_mpm/hooks/claude_hooks/tool_analysis.py +2 -2
  98. claude_mpm/hooks/memory_integration_hook.py +1 -1
  99. claude_mpm/hooks/validation_hooks.py +1 -1
  100. claude_mpm/init.py +4 -4
  101. claude_mpm/models/agent_session.py +1 -1
  102. claude_mpm/scripts/socketio_daemon.py +5 -5
  103. claude_mpm/services/__init__.py +145 -161
  104. claude_mpm/services/agent_capabilities_service.py +1 -1
  105. claude_mpm/services/agents/agent_builder.py +4 -4
  106. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +1 -1
  107. claude_mpm/services/agents/deployment/agent_metrics_collector.py +1 -1
  108. claude_mpm/services/agents/deployment/agent_record_service.py +3 -3
  109. claude_mpm/services/agents/deployment/deployment_config_loader.py +21 -0
  110. claude_mpm/services/agents/deployment/deployment_wrapper.py +1 -1
  111. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +2 -2
  112. claude_mpm/services/agents/loading/agent_profile_loader.py +2 -2
  113. claude_mpm/services/agents/loading/base_agent_manager.py +12 -2
  114. claude_mpm/services/agents/local_template_manager.py +5 -5
  115. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  116. claude_mpm/services/agents/registry/modification_tracker.py +19 -11
  117. claude_mpm/services/async_session_logger.py +3 -3
  118. claude_mpm/services/claude_session_logger.py +4 -4
  119. claude_mpm/services/cli/agent_listing_service.py +3 -3
  120. claude_mpm/services/cli/agent_validation_service.py +1 -1
  121. claude_mpm/services/cli/session_manager.py +2 -2
  122. claude_mpm/services/core/path_resolver.py +1 -1
  123. claude_mpm/services/diagnostics/checks/agent_check.py +1 -1
  124. claude_mpm/services/diagnostics/checks/claude_code_check.py +2 -2
  125. claude_mpm/services/diagnostics/checks/common_issues_check.py +3 -3
  126. claude_mpm/services/diagnostics/checks/configuration_check.py +2 -2
  127. claude_mpm/services/diagnostics/checks/installation_check.py +1 -1
  128. claude_mpm/services/diagnostics/checks/mcp_check.py +1 -1
  129. claude_mpm/services/diagnostics/checks/mcp_services_check.py +9 -9
  130. claude_mpm/services/diagnostics/checks/monitor_check.py +1 -1
  131. claude_mpm/services/diagnostics/doctor_reporter.py +1 -1
  132. claude_mpm/services/event_aggregator.py +1 -1
  133. claude_mpm/services/event_bus/event_bus.py +7 -2
  134. claude_mpm/services/events/consumers/dead_letter.py +2 -2
  135. claude_mpm/services/framework_claude_md_generator/__init__.py +1 -1
  136. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +3 -3
  137. claude_mpm/services/framework_claude_md_generator/version_manager.py +1 -1
  138. claude_mpm/services/hook_installer_service.py +7 -7
  139. claude_mpm/services/infrastructure/context_preservation.py +7 -7
  140. claude_mpm/services/infrastructure/daemon_manager.py +5 -5
  141. claude_mpm/services/mcp_config_manager.py +169 -48
  142. claude_mpm/services/mcp_gateway/__init__.py +98 -94
  143. claude_mpm/services/mcp_gateway/auto_configure.py +5 -5
  144. claude_mpm/services/mcp_gateway/config/config_loader.py +2 -2
  145. claude_mpm/services/mcp_gateway/config/configuration.py +3 -3
  146. claude_mpm/services/mcp_gateway/core/process_pool.py +3 -3
  147. claude_mpm/services/mcp_gateway/core/singleton_manager.py +2 -2
  148. claude_mpm/services/mcp_gateway/core/startup_verification.py +1 -1
  149. claude_mpm/services/mcp_gateway/main.py +1 -1
  150. claude_mpm/services/mcp_gateway/registry/service_registry.py +4 -2
  151. claude_mpm/services/mcp_gateway/registry/tool_registry.py +2 -1
  152. claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
  153. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
  154. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +1 -1
  155. claude_mpm/services/mcp_gateway/tools/hello_world.py +1 -1
  156. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +5 -5
  157. claude_mpm/services/mcp_gateway/utils/update_preferences.py +2 -2
  158. claude_mpm/services/mcp_service_verifier.py +1 -1
  159. claude_mpm/services/memory/builder.py +1 -1
  160. claude_mpm/services/memory/cache/shared_prompt_cache.py +2 -1
  161. claude_mpm/services/memory/indexed_memory.py +3 -3
  162. claude_mpm/services/monitor/daemon.py +1 -1
  163. claude_mpm/services/monitor/daemon_manager.py +9 -9
  164. claude_mpm/services/monitor/event_emitter.py +1 -1
  165. claude_mpm/services/monitor/handlers/file.py +1 -1
  166. claude_mpm/services/monitor/handlers/hooks.py +3 -3
  167. claude_mpm/services/monitor/management/lifecycle.py +7 -7
  168. claude_mpm/services/monitor/server.py +2 -2
  169. claude_mpm/services/orphan_detection.py +788 -0
  170. claude_mpm/services/port_manager.py +2 -2
  171. claude_mpm/services/project/analyzer.py +3 -3
  172. claude_mpm/services/project/archive_manager.py +13 -13
  173. claude_mpm/services/project/dependency_analyzer.py +4 -4
  174. claude_mpm/services/project/documentation_manager.py +4 -4
  175. claude_mpm/services/project/enhanced_analyzer.py +8 -8
  176. claude_mpm/services/project/registry.py +4 -4
  177. claude_mpm/services/project_port_allocator.py +597 -0
  178. claude_mpm/services/response_tracker.py +1 -1
  179. claude_mpm/services/session_management_service.py +1 -1
  180. claude_mpm/services/session_manager.py +6 -4
  181. claude_mpm/services/socketio/event_normalizer.py +1 -1
  182. claude_mpm/services/socketio/handlers/code_analysis.py +14 -12
  183. claude_mpm/services/socketio/handlers/file.py +1 -1
  184. claude_mpm/services/socketio/migration_utils.py +1 -1
  185. claude_mpm/services/socketio/server/core.py +1 -1
  186. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +1 -1
  187. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +4 -4
  188. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +1 -1
  189. claude_mpm/services/unified/config_strategies/config_schema.py +4 -4
  190. claude_mpm/services/unified/config_strategies/context_strategy.py +6 -6
  191. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +10 -10
  192. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +5 -5
  193. claude_mpm/services/unified/config_strategies/unified_config_service.py +8 -8
  194. claude_mpm/services/unified/config_strategies/validation_strategy.py +15 -15
  195. claude_mpm/services/unified/deployment_strategies/base.py +4 -4
  196. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +15 -15
  197. claude_mpm/services/unified/deployment_strategies/local.py +9 -9
  198. claude_mpm/services/unified/deployment_strategies/utils.py +9 -9
  199. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -7
  200. claude_mpm/services/unified/unified_config.py +5 -5
  201. claude_mpm/services/unified/unified_deployment.py +2 -2
  202. claude_mpm/services/utility_service.py +1 -1
  203. claude_mpm/services/version_control/conflict_resolution.py +2 -2
  204. claude_mpm/services/version_control/git_operations.py +3 -3
  205. claude_mpm/services/version_control/semantic_versioning.py +13 -13
  206. claude_mpm/services/version_control/version_parser.py +1 -1
  207. claude_mpm/storage/state_storage.py +12 -13
  208. claude_mpm/tools/code_tree_analyzer.py +5 -5
  209. claude_mpm/tools/code_tree_builder.py +4 -4
  210. claude_mpm/tools/socketio_debug.py +1 -1
  211. claude_mpm/utils/agent_dependency_loader.py +4 -4
  212. claude_mpm/utils/common.py +2 -2
  213. claude_mpm/utils/config_manager.py +3 -3
  214. claude_mpm/utils/dependency_cache.py +2 -2
  215. claude_mpm/utils/dependency_strategies.py +6 -6
  216. claude_mpm/utils/file_utils.py +11 -11
  217. claude_mpm/utils/log_cleanup.py +1 -1
  218. claude_mpm/utils/path_operations.py +1 -1
  219. claude_mpm/validation/agent_validator.py +2 -2
  220. claude_mpm/validation/frontmatter_validator.py +1 -1
  221. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/METADATA +1 -1
  222. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/RECORD +226 -223
  223. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/WHEEL +0 -0
  224. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/entry_points.txt +0 -0
  225. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/licenses/LICENSE +0 -0
  226. {claude_mpm-4.5.8.dist-info → claude_mpm-4.5.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Port Allocator Service
4
+ ==============================
5
+
6
+ Provides deterministic, hash-based port allocation for local development projects.
7
+ Ensures each project gets a consistent port across sessions while avoiding conflicts.
8
+
9
+ Part of local-ops agent improvements for single port per project allocation.
10
+
11
+ WHY: Manual port assignment is error-prone and leads to conflicts. Hash-based
12
+ allocation provides predictable, consistent port assignments while avoiding
13
+ collisions through linear probing.
14
+
15
+ DESIGN DECISIONS:
16
+ - Hash-based allocation: Projects get same port consistently (SHA-256 of path)
17
+ - Port range: 3000-3999 (1000 ports for user projects)
18
+ - Linear probing: Handles hash collisions gracefully
19
+ - Global registry: Prevents conflicts across multiple projects
20
+ - Persistent state: Survives restarts and maintains history
21
+ - Atomic operations: Prevents race conditions in port allocation
22
+ """
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any, Dict, Optional
30
+
31
+ import psutil
32
+
33
+ from .core.base import SyncBaseService
34
+
35
+
36
+ class ProjectPortAllocator(SyncBaseService):
37
+ """
38
+ Manages port allocation for local development projects.
39
+
40
+ Features:
41
+ - Deterministic port allocation based on project path hash
42
+ - Persistent state tracking across sessions
43
+ - Global registry to prevent cross-project conflicts
44
+ - Orphan detection and cleanup
45
+ - Linear probing for conflict resolution
46
+ """
47
+
48
+ # Port range for user projects (avoiding system ports and Claude MPM services)
49
+ DEFAULT_PORT_RANGE_START = 3000
50
+ DEFAULT_PORT_RANGE_END = 3999
51
+
52
+ # Claude MPM services use 8765-8785, keep these protected
53
+ PROTECTED_PORT_RANGES = [(8765, 8785)]
54
+
55
+ # State file names
56
+ STATE_FILE_NAME = "deployment-state.json"
57
+ GLOBAL_REGISTRY_FILE = "global-port-registry.json"
58
+
59
+ def __init__(
60
+ self,
61
+ project_root: Optional[Path] = None,
62
+ port_range_start: Optional[int] = None,
63
+ port_range_end: Optional[int] = None,
64
+ ):
65
+ """
66
+ Initialize the port allocator.
67
+
68
+ Args:
69
+ project_root: Project directory (default: current working directory)
70
+ port_range_start: Start of port range (default: 3000)
71
+ port_range_end: End of port range (default: 3999)
72
+ """
73
+ super().__init__(service_name="ProjectPortAllocator")
74
+
75
+ self.project_root = (project_root or Path.cwd()).resolve()
76
+ self.port_range_start = port_range_start or self.DEFAULT_PORT_RANGE_START
77
+ self.port_range_end = port_range_end or self.DEFAULT_PORT_RANGE_END
78
+
79
+ # Project-local state directory
80
+ self.state_dir = self.project_root / ".claude-mpm"
81
+ self.state_file = self.state_dir / self.STATE_FILE_NAME
82
+
83
+ # Global registry in user home directory
84
+ self.global_registry_dir = Path.home() / ".claude-mpm"
85
+ self.global_registry_file = self.global_registry_dir / self.GLOBAL_REGISTRY_FILE
86
+
87
+ # Ensure directories exist
88
+ self.state_dir.mkdir(parents=True, exist_ok=True)
89
+ self.global_registry_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ def initialize(self) -> bool:
92
+ """
93
+ Initialize the service.
94
+
95
+ Returns:
96
+ True if initialization successful
97
+ """
98
+ try:
99
+ # Cleanup any dead registrations on startup
100
+ self.cleanup_dead_registrations()
101
+ self._initialized = True
102
+ self.log_info("ProjectPortAllocator initialized successfully")
103
+ return True
104
+ except Exception as e:
105
+ self.log_error(f"Failed to initialize: {e}")
106
+ return False
107
+
108
+ def shutdown(self) -> None:
109
+ """Shutdown the service gracefully."""
110
+ self._shutdown = True
111
+ self.log_info("ProjectPortAllocator shutdown")
112
+
113
+ def _compute_project_hash(self, project_path: Path) -> str:
114
+ """
115
+ Compute deterministic hash for a project path.
116
+
117
+ Args:
118
+ project_path: Absolute path to project
119
+
120
+ Returns:
121
+ SHA-256 hash of the project path
122
+ """
123
+ # Use absolute path for consistency
124
+ absolute_path = project_path.resolve()
125
+ path_str = str(absolute_path)
126
+
127
+ # Compute SHA-256 hash
128
+ hash_obj = hashlib.sha256(path_str.encode("utf-8"))
129
+ return hash_obj.hexdigest()
130
+
131
+ def _hash_to_port(self, project_hash: str) -> int:
132
+ """
133
+ Convert project hash to a port number in the allowed range.
134
+
135
+ Args:
136
+ project_hash: SHA-256 hash of project path
137
+
138
+ Returns:
139
+ Port number in the configured range
140
+ """
141
+ # Use first 8 hex chars as integer
142
+ hash_int = int(project_hash[:8], 16)
143
+
144
+ # Map to port range
145
+ port_range = self.port_range_end - self.port_range_start + 1
146
+ return self.port_range_start + (hash_int % port_range)
147
+
148
+
149
+ def _is_port_available(self, port: int) -> bool:
150
+ """
151
+ Check if a port is available for binding.
152
+
153
+ Args:
154
+ port: Port number to check
155
+
156
+ Returns:
157
+ True if port is available
158
+ """
159
+ try:
160
+ import socket
161
+
162
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
163
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
164
+ sock.bind(("localhost", port))
165
+ return True
166
+ except OSError:
167
+ return False
168
+
169
+ def _is_protected_port(self, port: int) -> bool:
170
+ """
171
+ Check if port is in a protected range.
172
+
173
+ Args:
174
+ port: Port number to check
175
+
176
+ Returns:
177
+ True if port is protected
178
+ """
179
+ return any(start <= port <= end for start, end in self.PROTECTED_PORT_RANGES)
180
+
181
+ def _load_project_state(self) -> Dict[str, Any]:
182
+ """
183
+ Load project deployment state.
184
+
185
+ Returns:
186
+ State dictionary or empty dict if not found
187
+ """
188
+ try:
189
+ if self.state_file.exists():
190
+ with self.state_file.open() as f:
191
+ return json.load(f)
192
+ except Exception as e:
193
+ self.log_warning(f"Failed to load project state: {e}")
194
+
195
+ return {}
196
+
197
+ def _save_project_state(self, state: Dict[str, Any]) -> None:
198
+ """
199
+ Save project deployment state atomically.
200
+
201
+ Args:
202
+ state: State dictionary to save
203
+ """
204
+ try:
205
+ # Write to temporary file first
206
+ temp_file = self.state_file.with_suffix(".tmp")
207
+ with temp_file.open("w") as f:
208
+ json.dump(state, f, indent=2)
209
+
210
+ # Atomic rename
211
+ temp_file.replace(self.state_file)
212
+
213
+ except Exception as e:
214
+ self.log_error(f"Failed to save project state: {e}")
215
+ raise
216
+
217
+ def _load_global_registry(self) -> Dict[str, Any]:
218
+ """
219
+ Load global port registry.
220
+
221
+ Returns:
222
+ Registry dictionary or empty dict if not found
223
+ """
224
+ try:
225
+ if self.global_registry_file.exists():
226
+ with self.global_registry_file.open() as f:
227
+ return json.load(f)
228
+ except Exception as e:
229
+ self.log_warning(f"Failed to load global registry: {e}")
230
+
231
+ return {"allocations": {}, "last_updated": None}
232
+
233
+ def _save_global_registry(self, registry: Dict[str, Any]) -> None:
234
+ """
235
+ Save global port registry atomically.
236
+
237
+ Args:
238
+ registry: Registry dictionary to save
239
+ """
240
+ try:
241
+ # Update timestamp
242
+ registry["last_updated"] = datetime.now(timezone.utc).isoformat()
243
+
244
+ # Write to temporary file first
245
+ temp_file = self.global_registry_file.with_suffix(".tmp")
246
+ with temp_file.open("w") as f:
247
+ json.dump(registry, f, indent=2)
248
+
249
+ # Atomic rename
250
+ temp_file.replace(self.global_registry_file)
251
+
252
+ except Exception as e:
253
+ self.log_error(f"Failed to save global registry: {e}")
254
+ raise
255
+
256
+ def get_project_port(
257
+ self,
258
+ project_path: Optional[Path] = None,
259
+ service_name: str = "main",
260
+ respect_env_override: bool = True,
261
+ ) -> int:
262
+ """
263
+ Get the allocated port for a project service.
264
+
265
+ This is the main entry point for port allocation. It:
266
+ 1. Checks for environment variable override (PROJECT_PORT)
267
+ 2. Checks existing allocation in state files
268
+ 3. Computes hash-based port with linear probing for conflicts
269
+
270
+ Args:
271
+ project_path: Path to project (default: self.project_root)
272
+ service_name: Name of the service (default: "main")
273
+ respect_env_override: Whether to respect PROJECT_PORT env var
274
+
275
+ Returns:
276
+ Allocated port number
277
+
278
+ Raises:
279
+ RuntimeError: If no available port found
280
+ """
281
+ project_path = (project_path or self.project_root).resolve()
282
+
283
+ # Check environment variable override
284
+ if respect_env_override:
285
+ env_port = os.environ.get("PROJECT_PORT")
286
+ if env_port:
287
+ try:
288
+ port = int(env_port)
289
+ self.log_info(
290
+ f"Using port {port} from PROJECT_PORT environment variable"
291
+ )
292
+ return port
293
+ except ValueError:
294
+ self.log_warning(f"Invalid PROJECT_PORT value: {env_port}")
295
+
296
+ # Check existing allocation
297
+ state = self._load_project_state()
298
+ deployments = state.get("deployments", {})
299
+
300
+ if service_name in deployments:
301
+ existing_port = deployments[service_name].get("port")
302
+ if existing_port and self._is_port_available(existing_port):
303
+ self.log_info(
304
+ f"Reusing existing port {existing_port} for {service_name}"
305
+ )
306
+ return existing_port
307
+
308
+ # Compute hash-based port with linear probing
309
+ project_hash = self._compute_project_hash(project_path)
310
+ base_port = self._hash_to_port(project_hash)
311
+
312
+ # Try base port first
313
+ port = self._find_available_port(base_port, project_path, service_name)
314
+
315
+ self.log_info(
316
+ f"Allocated port {port} for {service_name} "
317
+ f"(hash: {project_hash[:8]}, base: {base_port})"
318
+ )
319
+
320
+ return port
321
+
322
+ def _find_available_port(
323
+ self,
324
+ start_port: int,
325
+ project_path: Path,
326
+ service_name: str,
327
+ ) -> int:
328
+ """
329
+ Find available port using linear probing.
330
+
331
+ Args:
332
+ start_port: Starting port from hash
333
+ project_path: Project path
334
+ service_name: Service name
335
+
336
+ Returns:
337
+ Available port number
338
+
339
+ Raises:
340
+ RuntimeError: If no available port found
341
+ """
342
+ max_probes = self.port_range_end - self.port_range_start + 1
343
+
344
+ for offset in range(max_probes):
345
+ port = start_port + offset
346
+
347
+ # Wrap around if we exceed range
348
+ if port > self.port_range_end:
349
+ port = self.port_range_start + (port - self.port_range_end - 1)
350
+
351
+ # Skip protected ports
352
+ if self._is_protected_port(port):
353
+ continue
354
+
355
+ # Check if port is available
356
+ if self._is_port_available(port):
357
+ return port
358
+
359
+ raise RuntimeError(
360
+ f"No available ports in range {self.port_range_start}-{self.port_range_end}"
361
+ )
362
+
363
+ def register_port(
364
+ self,
365
+ port: int,
366
+ service_name: str = "main",
367
+ deployment_info: Optional[Dict[str, Any]] = None,
368
+ project_path: Optional[Path] = None,
369
+ ) -> None:
370
+ """
371
+ Register a port allocation for a project service.
372
+
373
+ Args:
374
+ port: Port number
375
+ service_name: Service name
376
+ deployment_info: Additional deployment information
377
+ project_path: Project path (default: self.project_root)
378
+ """
379
+ project_path = (project_path or self.project_root).resolve()
380
+ project_hash = self._compute_project_hash(project_path)
381
+
382
+ # Update project state
383
+ state = self._load_project_state()
384
+
385
+ if "project_path" not in state:
386
+ state["project_path"] = str(project_path)
387
+ state["project_hash"] = project_hash
388
+ state["deployments"] = {}
389
+ state["port_history"] = []
390
+
391
+ # Merge deployment info
392
+ deployment_data = deployment_info or {}
393
+ deployment_data.update(
394
+ {
395
+ "port": port,
396
+ "service_name": service_name,
397
+ "registered_at": datetime.now(timezone.utc).isoformat(),
398
+ }
399
+ )
400
+
401
+ state["deployments"][service_name] = deployment_data
402
+
403
+ # Track port history
404
+ if port not in state.get("port_history", []):
405
+ state.setdefault("port_history", []).append(port)
406
+
407
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
408
+
409
+ self._save_project_state(state)
410
+
411
+ # Update global registry
412
+ registry = self._load_global_registry()
413
+
414
+ registry.setdefault("allocations", {})[str(port)] = {
415
+ "project_path": str(project_path),
416
+ "project_hash": project_hash,
417
+ "service_name": service_name,
418
+ "registered_at": datetime.now(timezone.utc).isoformat(),
419
+ }
420
+
421
+ self._save_global_registry(registry)
422
+
423
+ self.log_info(f"Registered port {port} for {service_name}")
424
+
425
+ def release_port(
426
+ self,
427
+ port: int,
428
+ service_name: str = "main",
429
+ project_path: Optional[Path] = None,
430
+ ) -> None:
431
+ """
432
+ Release a port allocation.
433
+
434
+ Args:
435
+ port: Port number to release
436
+ service_name: Service name
437
+ project_path: Project path (default: self.project_root)
438
+ """
439
+ project_path = (project_path or self.project_root).resolve()
440
+
441
+ # Update project state
442
+ state = self._load_project_state()
443
+ deployments = state.get("deployments", {})
444
+
445
+ if service_name in deployments:
446
+ del deployments[service_name]
447
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
448
+ self._save_project_state(state)
449
+
450
+ # Update global registry
451
+ registry = self._load_global_registry()
452
+ allocations = registry.get("allocations", {})
453
+
454
+ if str(port) in allocations:
455
+ del allocations[str(port)]
456
+ self._save_global_registry(registry)
457
+
458
+ self.log_info(f"Released port {port} for {service_name}")
459
+
460
+ def cleanup_dead_registrations(self) -> int:
461
+ """
462
+ Clean up registrations for dead processes.
463
+
464
+ Returns:
465
+ Number of registrations cleaned up
466
+ """
467
+ cleaned = 0
468
+
469
+ # Clean project state
470
+ state = self._load_project_state()
471
+ deployments = state.get("deployments", {})
472
+ dead_services = []
473
+
474
+ for service_name, deployment in deployments.items():
475
+ pid = deployment.get("pid")
476
+ if pid and not self._is_process_alive(pid):
477
+ dead_services.append(service_name)
478
+ cleaned += 1
479
+
480
+ for service_name in dead_services:
481
+ self.log_info(f"Cleaning up dead deployment: {service_name}")
482
+ del deployments[service_name]
483
+
484
+ if dead_services:
485
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
486
+ self._save_project_state(state)
487
+
488
+ # Clean global registry
489
+ registry = self._load_global_registry()
490
+ allocations = registry.get("allocations", {})
491
+ dead_ports = []
492
+
493
+ for port_str, allocation in allocations.items():
494
+ project_path = Path(allocation.get("project_path", ""))
495
+
496
+ # Check if project still exists
497
+ if not project_path.exists():
498
+ dead_ports.append(port_str)
499
+ cleaned += 1
500
+ continue
501
+
502
+ # Check project state file
503
+ state_file = project_path / ".claude-mpm" / self.STATE_FILE_NAME
504
+ if state_file.exists():
505
+ try:
506
+ with state_file.open() as f:
507
+ project_state = json.load(f)
508
+
509
+ # Check if service still registered
510
+ service_name = allocation.get("service_name")
511
+ if service_name not in project_state.get("deployments", {}):
512
+ dead_ports.append(port_str)
513
+ cleaned += 1
514
+
515
+ except Exception as e:
516
+ self.log_warning(f"Error checking state for port {port_str}: {e}")
517
+
518
+ for port_str in dead_ports:
519
+ self.log_info(f"Cleaning up dead global allocation: port {port_str}")
520
+ del allocations[port_str]
521
+
522
+ if dead_ports:
523
+ self._save_global_registry(registry)
524
+
525
+ if cleaned > 0:
526
+ self.log_info(f"Cleaned up {cleaned} dead registrations")
527
+
528
+ return cleaned
529
+
530
+ def _is_process_alive(self, pid: int) -> bool:
531
+ """
532
+ Check if a process is alive.
533
+
534
+ Args:
535
+ pid: Process ID
536
+
537
+ Returns:
538
+ True if process is alive
539
+ """
540
+ try:
541
+ return psutil.pid_exists(pid)
542
+ except Exception:
543
+ return False
544
+
545
+ def get_allocation_info(
546
+ self,
547
+ service_name: str = "main",
548
+ project_path: Optional[Path] = None,
549
+ ) -> Optional[Dict[str, Any]]:
550
+ """
551
+ Get allocation information for a service.
552
+
553
+ Args:
554
+ service_name: Service name
555
+ project_path: Project path (default: self.project_root)
556
+
557
+ Returns:
558
+ Allocation info dict or None if not found
559
+ """
560
+ project_path = (project_path or self.project_root).resolve()
561
+ state = self._load_project_state()
562
+ deployments = state.get("deployments", {})
563
+
564
+ return deployments.get(service_name)
565
+
566
+ def list_project_allocations(
567
+ self,
568
+ project_path: Optional[Path] = None,
569
+ ) -> Dict[str, Any]:
570
+ """
571
+ List all port allocations for a project.
572
+
573
+ Args:
574
+ project_path: Project path (default: self.project_root)
575
+
576
+ Returns:
577
+ Dictionary of service allocations
578
+ """
579
+ project_path = (project_path or self.project_root).resolve()
580
+ state = self._load_project_state()
581
+
582
+ return {
583
+ "project_path": str(project_path),
584
+ "project_hash": state.get("project_hash"),
585
+ "deployments": state.get("deployments", {}),
586
+ "port_history": state.get("port_history", []),
587
+ "last_updated": state.get("last_updated"),
588
+ }
589
+
590
+ def list_global_allocations(self) -> Dict[str, Any]:
591
+ """
592
+ List all global port allocations.
593
+
594
+ Returns:
595
+ Global registry data
596
+ """
597
+ return self._load_global_registry()
@@ -80,7 +80,7 @@ class ResponseTracker:
80
80
  from claude_mpm.services.claude_session_logger import get_session_logger
81
81
 
82
82
  self.session_logger = get_session_logger(config)
83
- logger.info(
83
+ logger.debug(
84
84
  f"Response tracker initialized with base directory: {base_dir}"
85
85
  )
86
86
  except Exception as e:
@@ -187,7 +187,7 @@ class SessionManagementService(BaseService, SessionManagementInterface):
187
187
  event_data["timestamp"] = datetime.now(timezone.utc).isoformat()
188
188
 
189
189
  # Append to log file as JSONL
190
- with open(log_file, "a") as f:
190
+ with log_file.open("a") as f:
191
191
  f.write(json.dumps(event_data) + "\n")
192
192
 
193
193
  except Exception as e:
@@ -67,7 +67,7 @@ class SessionManager:
67
67
  # Mark as initialized
68
68
  self.__class__._initialized = True
69
69
 
70
- logger.info(
70
+ logger.debug(
71
71
  f"SessionManager initialized with session ID: {self._session_id}"
72
72
  )
73
73
 
@@ -86,12 +86,12 @@ class SessionManager:
86
86
  for env_var in env_vars:
87
87
  session_id = os.environ.get(env_var)
88
88
  if session_id:
89
- logger.info(f"Using session ID from {env_var}: {session_id}")
89
+ logger.debug(f"Using session ID from {env_var}: {session_id}")
90
90
  return session_id
91
91
 
92
92
  # Generate timestamp-based session ID
93
93
  session_id = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
94
- logger.info(f"Generated new session ID: {session_id}")
94
+ logger.debug(f"Generated new session ID: {session_id}")
95
95
  return session_id
96
96
 
97
97
  def get_session_id(self) -> str:
@@ -130,7 +130,9 @@ class SessionManager:
130
130
  self._session_id = session_id
131
131
  logger.warning(f"Session ID changed from {old_id} to {session_id}")
132
132
  else:
133
- logger.debug(f"Session ID already set to {session_id}, no change needed")
133
+ logger.debug(
134
+ f"Session ID already set to {session_id}, no change needed"
135
+ )
134
136
 
135
137
  @classmethod
136
138
  def reset(cls) -> None:
@@ -342,7 +342,7 @@ class EventNormalizer:
342
342
 
343
343
  return "unknown"
344
344
 
345
- def _map_event_name(self, event_name: str) -> Tuple[str, str]: # noqa: PLR0911
345
+ def _map_event_name(self, event_name: str) -> Tuple[str, str]:
346
346
  """Map event name to (type, subtype) tuple.
347
347
 
348
348
  WHY: Consistent categorization helps clients filter and handle events.