claude-mpm 3.9.11__py3-none-any.whl → 4.0.4__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 (434) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +2 -2
  3. claude_mpm/__main__.py +3 -2
  4. claude_mpm/agents/__init__.py +85 -79
  5. claude_mpm/agents/agent_loader.py +464 -1003
  6. claude_mpm/agents/agent_loader_integration.py +45 -45
  7. claude_mpm/agents/agents_metadata.py +29 -30
  8. claude_mpm/agents/async_agent_loader.py +156 -138
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/base_agent_loader.py +179 -151
  11. claude_mpm/agents/frontmatter_validator.py +229 -130
  12. claude_mpm/agents/schema/agent_schema.json +1 -1
  13. claude_mpm/agents/system_agent_config.py +213 -147
  14. claude_mpm/agents/templates/__init__.py +13 -13
  15. claude_mpm/agents/templates/code_analyzer.json +2 -2
  16. claude_mpm/agents/templates/data_engineer.json +1 -1
  17. claude_mpm/agents/templates/documentation.json +23 -11
  18. claude_mpm/agents/templates/engineer.json +22 -6
  19. claude_mpm/agents/templates/memory_manager.json +1 -1
  20. claude_mpm/agents/templates/ops.json +2 -2
  21. claude_mpm/agents/templates/project_organizer.json +1 -1
  22. claude_mpm/agents/templates/qa.json +1 -1
  23. claude_mpm/agents/templates/refactoring_engineer.json +222 -0
  24. claude_mpm/agents/templates/research.json +20 -14
  25. claude_mpm/agents/templates/security.json +1 -1
  26. claude_mpm/agents/templates/ticketing.json +2 -2
  27. claude_mpm/agents/templates/version_control.json +1 -1
  28. claude_mpm/agents/templates/web_qa.json +3 -1
  29. claude_mpm/agents/templates/web_ui.json +2 -2
  30. claude_mpm/cli/__init__.py +79 -51
  31. claude_mpm/cli/__main__.py +3 -2
  32. claude_mpm/cli/commands/__init__.py +20 -20
  33. claude_mpm/cli/commands/agents.py +279 -247
  34. claude_mpm/cli/commands/aggregate.py +138 -157
  35. claude_mpm/cli/commands/cleanup.py +147 -147
  36. claude_mpm/cli/commands/config.py +93 -76
  37. claude_mpm/cli/commands/info.py +17 -16
  38. claude_mpm/cli/commands/mcp.py +140 -905
  39. claude_mpm/cli/commands/mcp_command_router.py +139 -0
  40. claude_mpm/cli/commands/mcp_config_commands.py +20 -0
  41. claude_mpm/cli/commands/mcp_install_commands.py +20 -0
  42. claude_mpm/cli/commands/mcp_server_commands.py +175 -0
  43. claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
  44. claude_mpm/cli/commands/memory.py +239 -203
  45. claude_mpm/cli/commands/monitor.py +330 -86
  46. claude_mpm/cli/commands/run.py +380 -429
  47. claude_mpm/cli/commands/run_config_checker.py +160 -0
  48. claude_mpm/cli/commands/socketio_monitor.py +235 -0
  49. claude_mpm/cli/commands/tickets.py +363 -220
  50. claude_mpm/cli/parser.py +24 -1156
  51. claude_mpm/cli/parsers/__init__.py +29 -0
  52. claude_mpm/cli/parsers/agents_parser.py +136 -0
  53. claude_mpm/cli/parsers/base_parser.py +331 -0
  54. claude_mpm/cli/parsers/config_parser.py +85 -0
  55. claude_mpm/cli/parsers/mcp_parser.py +152 -0
  56. claude_mpm/cli/parsers/memory_parser.py +138 -0
  57. claude_mpm/cli/parsers/monitor_parser.py +124 -0
  58. claude_mpm/cli/parsers/run_parser.py +147 -0
  59. claude_mpm/cli/parsers/tickets_parser.py +203 -0
  60. claude_mpm/cli/ticket_cli.py +7 -3
  61. claude_mpm/cli/utils.py +55 -37
  62. claude_mpm/cli_module/__init__.py +6 -6
  63. claude_mpm/cli_module/args.py +188 -140
  64. claude_mpm/cli_module/commands.py +79 -70
  65. claude_mpm/cli_module/migration_example.py +38 -60
  66. claude_mpm/config/__init__.py +32 -25
  67. claude_mpm/config/agent_config.py +151 -119
  68. claude_mpm/config/experimental_features.py +71 -73
  69. claude_mpm/config/paths.py +94 -208
  70. claude_mpm/config/socketio_config.py +84 -73
  71. claude_mpm/constants.py +35 -18
  72. claude_mpm/core/__init__.py +9 -6
  73. claude_mpm/core/agent_name_normalizer.py +68 -71
  74. claude_mpm/core/agent_registry.py +372 -521
  75. claude_mpm/core/agent_session_manager.py +74 -63
  76. claude_mpm/core/base_service.py +116 -87
  77. claude_mpm/core/cache.py +119 -153
  78. claude_mpm/core/claude_runner.py +425 -1120
  79. claude_mpm/core/config.py +263 -168
  80. claude_mpm/core/config_aliases.py +69 -61
  81. claude_mpm/core/config_constants.py +292 -0
  82. claude_mpm/core/constants.py +57 -99
  83. claude_mpm/core/container.py +211 -178
  84. claude_mpm/core/exceptions.py +233 -89
  85. claude_mpm/core/factories.py +92 -54
  86. claude_mpm/core/framework_loader.py +378 -220
  87. claude_mpm/core/hook_manager.py +198 -83
  88. claude_mpm/core/hook_performance_config.py +136 -0
  89. claude_mpm/core/injectable_service.py +61 -55
  90. claude_mpm/core/interactive_session.py +165 -155
  91. claude_mpm/core/interfaces.py +221 -195
  92. claude_mpm/core/lazy.py +96 -96
  93. claude_mpm/core/logger.py +133 -107
  94. claude_mpm/core/logging_config.py +185 -157
  95. claude_mpm/core/minimal_framework_loader.py +20 -15
  96. claude_mpm/core/mixins.py +30 -29
  97. claude_mpm/core/oneshot_session.py +215 -181
  98. claude_mpm/core/optimized_agent_loader.py +134 -138
  99. claude_mpm/core/optimized_startup.py +159 -157
  100. claude_mpm/core/pm_hook_interceptor.py +85 -72
  101. claude_mpm/core/service_registry.py +103 -101
  102. claude_mpm/core/session_manager.py +97 -87
  103. claude_mpm/core/socketio_pool.py +212 -158
  104. claude_mpm/core/tool_access_control.py +58 -51
  105. claude_mpm/core/types.py +46 -24
  106. claude_mpm/core/typing_utils.py +166 -82
  107. claude_mpm/core/unified_agent_registry.py +721 -0
  108. claude_mpm/core/unified_config.py +550 -0
  109. claude_mpm/core/unified_paths.py +549 -0
  110. claude_mpm/dashboard/index.html +1 -1
  111. claude_mpm/dashboard/open_dashboard.py +51 -17
  112. claude_mpm/dashboard/static/built/components/agent-inference.js +2 -0
  113. claude_mpm/dashboard/static/built/components/event-processor.js +2 -0
  114. claude_mpm/dashboard/static/built/components/event-viewer.js +2 -0
  115. claude_mpm/dashboard/static/built/components/export-manager.js +2 -0
  116. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +2 -0
  117. claude_mpm/dashboard/static/built/components/hud-library-loader.js +2 -0
  118. claude_mpm/dashboard/static/built/components/hud-manager.js +2 -0
  119. claude_mpm/dashboard/static/built/components/hud-visualizer.js +2 -0
  120. claude_mpm/dashboard/static/built/components/module-viewer.js +2 -0
  121. claude_mpm/dashboard/static/built/components/session-manager.js +2 -0
  122. claude_mpm/dashboard/static/built/components/socket-manager.js +2 -0
  123. claude_mpm/dashboard/static/built/components/ui-state-manager.js +2 -0
  124. claude_mpm/dashboard/static/built/components/working-directory.js +2 -0
  125. claude_mpm/dashboard/static/built/dashboard.js +2 -0
  126. claude_mpm/dashboard/static/built/socket-client.js +2 -0
  127. claude_mpm/dashboard/static/css/dashboard.css +27 -8
  128. claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
  129. claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
  130. claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
  131. claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
  132. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
  133. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
  134. claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
  135. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
  136. claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
  137. claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
  138. claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
  139. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
  140. claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
  141. claude_mpm/dashboard/static/dist/dashboard.js +2 -0
  142. claude_mpm/dashboard/static/dist/socket-client.js +2 -0
  143. claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
  144. claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
  145. claude_mpm/dashboard/static/js/components/event-viewer.js +93 -72
  146. claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
  147. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +110 -96
  148. claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
  149. claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
  150. claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
  151. claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
  152. claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
  153. claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
  154. claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
  155. claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
  156. claude_mpm/dashboard/static/js/dashboard.js +178 -453
  157. claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
  158. claude_mpm/dashboard/static/js/socket-client.js +133 -53
  159. claude_mpm/dashboard/templates/index.html +40 -50
  160. claude_mpm/experimental/cli_enhancements.py +60 -58
  161. claude_mpm/generators/__init__.py +1 -1
  162. claude_mpm/generators/agent_profile_generator.py +75 -65
  163. claude_mpm/hooks/__init__.py +1 -1
  164. claude_mpm/hooks/base_hook.py +33 -28
  165. claude_mpm/hooks/claude_hooks/__init__.py +1 -1
  166. claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
  167. claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
  168. claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
  169. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
  170. claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
  171. claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
  172. claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
  173. claude_mpm/hooks/memory_integration_hook.py +140 -100
  174. claude_mpm/hooks/tool_call_interceptor.py +89 -76
  175. claude_mpm/hooks/validation_hooks.py +57 -49
  176. claude_mpm/init.py +145 -121
  177. claude_mpm/models/__init__.py +9 -9
  178. claude_mpm/models/agent_definition.py +33 -23
  179. claude_mpm/models/agent_session.py +228 -200
  180. claude_mpm/scripts/__init__.py +1 -1
  181. claude_mpm/scripts/socketio_daemon.py +192 -75
  182. claude_mpm/scripts/socketio_server_manager.py +328 -0
  183. claude_mpm/scripts/start_activity_logging.py +25 -22
  184. claude_mpm/services/__init__.py +68 -43
  185. claude_mpm/services/agent_capabilities_service.py +271 -0
  186. claude_mpm/services/agents/__init__.py +23 -32
  187. claude_mpm/services/agents/deployment/__init__.py +3 -3
  188. claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
  189. claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
  190. claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
  191. claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
  192. claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
  193. claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
  194. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
  195. claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
  196. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
  197. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
  198. claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
  199. claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
  200. claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
  201. claude_mpm/services/agents/deployment/agent_validator.py +352 -0
  202. claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
  203. claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
  204. claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
  205. claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
  206. claude_mpm/services/agents/deployment/config/__init__.py +13 -0
  207. claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
  208. claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
  209. claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
  210. claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
  211. claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
  212. claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
  213. claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
  214. claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
  215. claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
  216. claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
  217. claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
  218. claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
  219. claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
  220. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
  221. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
  222. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
  223. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
  224. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
  225. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
  226. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
  227. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
  228. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
  229. claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
  230. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
  231. claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
  232. claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
  233. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
  234. claude_mpm/services/agents/deployment/results/__init__.py +13 -0
  235. claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
  236. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
  237. claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
  238. claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
  239. claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
  240. claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
  241. claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
  242. claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
  243. claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
  244. claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
  245. claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
  246. claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
  247. claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
  248. claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
  249. claude_mpm/services/agents/loading/__init__.py +2 -2
  250. claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
  251. claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
  252. claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
  253. claude_mpm/services/agents/management/__init__.py +2 -2
  254. claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
  255. claude_mpm/services/agents/management/agent_management_service.py +209 -156
  256. claude_mpm/services/agents/memory/__init__.py +9 -6
  257. claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
  258. claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
  259. claude_mpm/services/agents/memory/analyzer.py +430 -0
  260. claude_mpm/services/agents/memory/content_manager.py +376 -0
  261. claude_mpm/services/agents/memory/template_generator.py +468 -0
  262. claude_mpm/services/agents/registry/__init__.py +7 -10
  263. claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
  264. claude_mpm/services/agents/registry/modification_tracker.py +351 -285
  265. claude_mpm/services/async_session_logger.py +187 -153
  266. claude_mpm/services/claude_session_logger.py +87 -72
  267. claude_mpm/services/command_handler_service.py +217 -0
  268. claude_mpm/services/communication/__init__.py +3 -2
  269. claude_mpm/services/core/__init__.py +50 -97
  270. claude_mpm/services/core/base.py +60 -53
  271. claude_mpm/services/core/interfaces/__init__.py +188 -0
  272. claude_mpm/services/core/interfaces/agent.py +351 -0
  273. claude_mpm/services/core/interfaces/communication.py +343 -0
  274. claude_mpm/services/core/interfaces/infrastructure.py +413 -0
  275. claude_mpm/services/core/interfaces/service.py +434 -0
  276. claude_mpm/services/core/interfaces.py +19 -944
  277. claude_mpm/services/event_aggregator.py +208 -170
  278. claude_mpm/services/exceptions.py +387 -308
  279. claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
  280. claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
  281. claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
  282. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
  283. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
  284. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
  285. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
  286. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
  287. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
  288. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
  289. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
  290. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
  291. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
  292. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
  293. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
  294. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
  295. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
  296. claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
  297. claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
  298. claude_mpm/services/hook_service.py +106 -114
  299. claude_mpm/services/infrastructure/__init__.py +7 -5
  300. claude_mpm/services/infrastructure/context_preservation.py +233 -199
  301. claude_mpm/services/infrastructure/daemon_manager.py +279 -0
  302. claude_mpm/services/infrastructure/logging.py +83 -76
  303. claude_mpm/services/infrastructure/monitoring.py +547 -404
  304. claude_mpm/services/mcp_gateway/__init__.py +30 -13
  305. claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
  306. claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
  307. claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
  308. claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
  309. claude_mpm/services/mcp_gateway/core/__init__.py +13 -20
  310. claude_mpm/services/mcp_gateway/core/base.py +80 -67
  311. claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
  312. claude_mpm/services/mcp_gateway/core/interfaces.py +87 -84
  313. claude_mpm/services/mcp_gateway/main.py +287 -137
  314. claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
  315. claude_mpm/services/mcp_gateway/registry/service_registry.py +97 -94
  316. claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
  317. claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
  318. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +105 -110
  319. claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
  320. claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
  321. claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
  322. claude_mpm/services/mcp_gateway/tools/base_adapter.py +109 -119
  323. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
  324. claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
  325. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
  326. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
  327. claude_mpm/services/memory/__init__.py +2 -2
  328. claude_mpm/services/memory/builder.py +451 -362
  329. claude_mpm/services/memory/cache/__init__.py +2 -2
  330. claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
  331. claude_mpm/services/memory/cache/simple_cache.py +107 -93
  332. claude_mpm/services/memory/indexed_memory.py +195 -193
  333. claude_mpm/services/memory/optimizer.py +267 -234
  334. claude_mpm/services/memory/router.py +571 -263
  335. claude_mpm/services/memory_hook_service.py +237 -0
  336. claude_mpm/services/port_manager.py +575 -0
  337. claude_mpm/services/project/__init__.py +3 -3
  338. claude_mpm/services/project/analyzer.py +451 -305
  339. claude_mpm/services/project/registry.py +262 -240
  340. claude_mpm/services/recovery_manager.py +287 -231
  341. claude_mpm/services/response_tracker.py +87 -67
  342. claude_mpm/services/runner_configuration_service.py +587 -0
  343. claude_mpm/services/session_management_service.py +304 -0
  344. claude_mpm/services/socketio/__init__.py +4 -4
  345. claude_mpm/services/socketio/client_proxy.py +174 -0
  346. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  347. claude_mpm/services/socketio/handlers/base.py +44 -30
  348. claude_mpm/services/socketio/handlers/connection.py +166 -64
  349. claude_mpm/services/socketio/handlers/file.py +123 -108
  350. claude_mpm/services/socketio/handlers/git.py +607 -373
  351. claude_mpm/services/socketio/handlers/hook.py +185 -0
  352. claude_mpm/services/socketio/handlers/memory.py +4 -4
  353. claude_mpm/services/socketio/handlers/project.py +4 -4
  354. claude_mpm/services/socketio/handlers/registry.py +53 -38
  355. claude_mpm/services/socketio/server/__init__.py +18 -0
  356. claude_mpm/services/socketio/server/broadcaster.py +252 -0
  357. claude_mpm/services/socketio/server/core.py +399 -0
  358. claude_mpm/services/socketio/server/main.py +323 -0
  359. claude_mpm/services/socketio_client_manager.py +160 -133
  360. claude_mpm/services/socketio_server.py +36 -1885
  361. claude_mpm/services/subprocess_launcher_service.py +316 -0
  362. claude_mpm/services/system_instructions_service.py +258 -0
  363. claude_mpm/services/ticket_manager.py +19 -533
  364. claude_mpm/services/utility_service.py +285 -0
  365. claude_mpm/services/version_control/__init__.py +18 -21
  366. claude_mpm/services/version_control/branch_strategy.py +20 -10
  367. claude_mpm/services/version_control/conflict_resolution.py +37 -13
  368. claude_mpm/services/version_control/git_operations.py +52 -21
  369. claude_mpm/services/version_control/semantic_versioning.py +92 -53
  370. claude_mpm/services/version_control/version_parser.py +145 -125
  371. claude_mpm/services/version_service.py +270 -0
  372. claude_mpm/storage/__init__.py +2 -2
  373. claude_mpm/storage/state_storage.py +177 -181
  374. claude_mpm/ticket_wrapper.py +2 -2
  375. claude_mpm/utils/__init__.py +2 -2
  376. claude_mpm/utils/agent_dependency_loader.py +453 -243
  377. claude_mpm/utils/config_manager.py +157 -118
  378. claude_mpm/utils/console.py +1 -1
  379. claude_mpm/utils/dependency_cache.py +102 -107
  380. claude_mpm/utils/dependency_manager.py +52 -47
  381. claude_mpm/utils/dependency_strategies.py +131 -96
  382. claude_mpm/utils/environment_context.py +110 -102
  383. claude_mpm/utils/error_handler.py +75 -55
  384. claude_mpm/utils/file_utils.py +80 -67
  385. claude_mpm/utils/framework_detection.py +12 -11
  386. claude_mpm/utils/import_migration_example.py +12 -60
  387. claude_mpm/utils/imports.py +48 -45
  388. claude_mpm/utils/path_operations.py +100 -93
  389. claude_mpm/utils/robust_installer.py +172 -164
  390. claude_mpm/utils/session_logging.py +30 -23
  391. claude_mpm/utils/subprocess_utils.py +99 -61
  392. claude_mpm/validation/__init__.py +1 -1
  393. claude_mpm/validation/agent_validator.py +151 -111
  394. claude_mpm/validation/frontmatter_validator.py +92 -71
  395. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.4.dist-info}/METADATA +90 -22
  396. claude_mpm-4.0.4.dist-info/RECORD +417 -0
  397. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.4.dist-info}/entry_points.txt +1 -0
  398. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.4.dist-info}/licenses/LICENSE +1 -1
  399. claude_mpm/cli/commands/run_guarded.py +0 -511
  400. claude_mpm/config/memory_guardian_config.py +0 -325
  401. claude_mpm/config/memory_guardian_yaml.py +0 -335
  402. claude_mpm/core/config_paths.py +0 -150
  403. claude_mpm/core/memory_aware_runner.py +0 -353
  404. claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
  405. claude_mpm/deployment_paths.py +0 -261
  406. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
  407. claude_mpm/models/state_models.py +0 -433
  408. claude_mpm/services/agent/__init__.py +0 -24
  409. claude_mpm/services/agent/deployment.py +0 -2548
  410. claude_mpm/services/agent/management.py +0 -598
  411. claude_mpm/services/agent/registry.py +0 -813
  412. claude_mpm/services/agents/registry/agent_registry.py +0 -813
  413. claude_mpm/services/communication/socketio.py +0 -1935
  414. claude_mpm/services/communication/websocket.py +0 -479
  415. claude_mpm/services/framework_claude_md_generator.py +0 -624
  416. claude_mpm/services/health_monitor.py +0 -893
  417. claude_mpm/services/infrastructure/graceful_degradation.py +0 -616
  418. claude_mpm/services/infrastructure/health_monitor.py +0 -775
  419. claude_mpm/services/infrastructure/memory_dashboard.py +0 -479
  420. claude_mpm/services/infrastructure/memory_guardian.py +0 -944
  421. claude_mpm/services/infrastructure/restart_protection.py +0 -642
  422. claude_mpm/services/infrastructure/state_manager.py +0 -774
  423. claude_mpm/services/mcp_gateway/manager.py +0 -334
  424. claude_mpm/services/optimized_hook_service.py +0 -542
  425. claude_mpm/services/project_analyzer.py +0 -864
  426. claude_mpm/services/project_registry.py +0 -608
  427. claude_mpm/services/standalone_socketio_server.py +0 -1300
  428. claude_mpm/services/ticket_manager_di.py +0 -318
  429. claude_mpm/services/ticketing_service_original.py +0 -510
  430. claude_mpm/utils/paths.py +0 -395
  431. claude_mpm/utils/platform_memory.py +0 -524
  432. claude_mpm-3.9.11.dist-info/RECORD +0 -306
  433. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.4.dist-info}/WHEEL +0 -0
  434. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.4.dist-info}/top_level.txt +0 -0
@@ -1,1300 +0,0 @@
1
- """Standalone Socket.IO server with independent versioning and deployment agnostic design.
2
-
3
- This server is designed to run independently of claude-mpm and maintain its own versioning.
4
- It provides a persistent Socket.IO service that can handle multiple claude-mpm client connections.
5
-
6
- KEY DESIGN PRINCIPLES:
7
- 1. Single server per machine - Only one instance should run
8
- 2. Persistent across sessions - Server keeps running when code is pushed
9
- 3. Separate versioning - Server has its own version schema independent of claude-mpm
10
- 4. Version compatibility mapping - Track which server versions work with which claude-mpm versions
11
- 5. Deployment agnostic - Works with local script, PyPI, npm installations
12
-
13
- WHY standalone architecture:
14
- - Allows server evolution independent of claude-mpm releases
15
- - Enables persistent monitoring across multiple claude-mpm sessions
16
- - Provides better resource management (one server vs multiple)
17
- - Simplifies debugging and maintenance
18
- - Supports different installation methods (PyPI, local, Docker, etc.)
19
- """
20
-
21
- import asyncio
22
- import json
23
- import logging
24
- import os
25
- import signal
26
- import socket
27
- import sys
28
- import threading
29
- import time
30
- import uuid
31
- from datetime import datetime
32
- from pathlib import Path
33
- from typing import Dict, Any, Optional, List, Set
34
- from collections import deque
35
- import importlib.metadata
36
- import fcntl # Unix file locking
37
- import platform
38
-
39
- # Import health monitoring and recovery systems
40
- try:
41
- from .health_monitor import (
42
- AdvancedHealthMonitor, ProcessResourceChecker,
43
- NetworkConnectivityChecker, ServiceHealthChecker,
44
- HealthStatus, HealthCheckResult
45
- )
46
- from .recovery_manager import RecoveryManager, RecoveryEvent
47
- HEALTH_MONITORING_AVAILABLE = True
48
- except ImportError as e:
49
- HEALTH_MONITORING_AVAILABLE = False
50
- # Create stub classes to prevent errors
51
- class AdvancedHealthMonitor:
52
- def __init__(self, *args, **kwargs): pass
53
- def add_checker(self, *args): pass
54
- def start_monitoring(self): pass
55
- async def stop_monitoring(self): pass
56
- def get_current_status(self): return None
57
- def export_diagnostics(self): return {}
58
-
59
- class RecoveryManager:
60
- def __init__(self, *args, **kwargs): pass
61
- def handle_health_result(self, *args): return None
62
- def get_recovery_status(self): return {}
63
-
64
- # Import enhanced error classes
65
- try:
66
- from .exceptions import (
67
- DaemonConflictError, PortConflictError, StaleProcessError,
68
- RecoveryFailedError, HealthCheckError, format_troubleshooting_guide
69
- )
70
- ENHANCED_ERRORS_AVAILABLE = True
71
- except ImportError as e:
72
- ENHANCED_ERRORS_AVAILABLE = False
73
- # Create stub classes to prevent errors
74
- class DaemonConflictError(Exception): pass
75
- class PortConflictError(Exception): pass
76
- class StaleProcessError(Exception): pass
77
- class RecoveryFailedError(Exception): pass
78
- class HealthCheckError(Exception): pass
79
- def format_troubleshooting_guide(error): return str(error)
80
-
81
- try:
82
- import psutil
83
- PSUTIL_AVAILABLE = True
84
- except ImportError:
85
- PSUTIL_AVAILABLE = False
86
- psutil = None
87
-
88
- # Windows file locking support
89
- if platform.system() == 'Windows':
90
- try:
91
- import msvcrt
92
- WINDOWS_LOCKING = True
93
- except ImportError:
94
- WINDOWS_LOCKING = False
95
- else:
96
- WINDOWS_LOCKING = False
97
- msvcrt = None
98
-
99
- try:
100
- import socketio
101
- from aiohttp import web
102
- SOCKETIO_AVAILABLE = True
103
-
104
- # Get Socket.IO version
105
- try:
106
- SOCKETIO_VERSION = importlib.metadata.version('python-socketio')
107
- except Exception:
108
- SOCKETIO_VERSION = 'unknown'
109
-
110
- except ImportError:
111
- SOCKETIO_AVAILABLE = False
112
- socketio = None
113
- web = None
114
- SOCKETIO_VERSION = 'not-installed'
115
-
116
- # Standalone server version - independent of claude-mpm
117
- STANDALONE_SERVER_VERSION = "1.0.0"
118
-
119
- # Compatibility matrix - which server versions work with which claude-mpm versions
120
- COMPATIBILITY_MATRIX = {
121
- "1.0.0": {
122
- "claude_mpm_versions": [">=0.7.0"],
123
- "min_python": "3.8",
124
- "socketio_min": "5.11.0",
125
- "features": [
126
- "persistent_server",
127
- "version_compatibility",
128
- "process_isolation",
129
- "health_monitoring",
130
- "advanced_health_monitoring",
131
- "automatic_recovery",
132
- "circuit_breaker",
133
- "resource_monitoring",
134
- "event_namespacing",
135
- "comprehensive_diagnostics",
136
- "metrics_export"
137
- ]
138
- }
139
- }
140
-
141
-
142
- class StandaloneSocketIOServer:
143
- """Standalone Socket.IO server with independent lifecycle and versioning.
144
-
145
- This server runs independently of claude-mpm processes and provides:
146
- - Version compatibility checking
147
- - Process isolation and management
148
- - Persistent operation across claude-mpm sessions
149
- - Health monitoring and diagnostics
150
- - Event namespacing and routing
151
- """
152
-
153
- def __init__(self, host: str = "localhost", port: int = 8765,
154
- server_id: Optional[str] = None):
155
- self.server_version = STANDALONE_SERVER_VERSION
156
- self.server_id = server_id or f"socketio-{uuid.uuid4().hex[:8]}"
157
- self.host = host
158
- self.port = port
159
- self.start_time = datetime.utcnow()
160
-
161
- # Setup logging
162
- self.logger = self._setup_logging()
163
-
164
- # Server state
165
- self.running = False
166
- self.clients: Set[str] = set()
167
- self.event_history: deque = deque(maxlen=10000) # Larger history for standalone server
168
- self.client_versions: Dict[str, str] = {} # Track client claude-mpm versions
169
- self.health_stats = {
170
- "events_processed": 0,
171
- "clients_served": 0,
172
- "errors": 0,
173
- "last_activity": None
174
- }
175
-
176
- # Asyncio components
177
- self.loop = None
178
- self.app = None
179
- self.sio = None
180
- self.runner = None
181
- self.site = None
182
-
183
- # Process management
184
- self.pid = os.getpid()
185
- self.pidfile_path = self._get_pidfile_path()
186
- self.pidfile_lock = None # File lock object
187
- self.process_start_time = None
188
- if PSUTIL_AVAILABLE:
189
- try:
190
- current_process = psutil.Process(self.pid)
191
- self.process_start_time = current_process.create_time()
192
- except Exception as e:
193
- self.logger.warning(f"Could not get process start time: {e}")
194
-
195
- if not SOCKETIO_AVAILABLE:
196
- self.logger.error("Socket.IO dependencies not available. Install with: pip install python-socketio aiohttp")
197
- return
198
-
199
- # Log initialization with comprehensive info
200
- self.logger.info(f"Standalone Socket.IO server v{self.server_version} initialized")
201
- self.logger.info(f"Server ID: {self.server_id}, PID: {self.pid}")
202
- self.logger.info(f"Using python-socketio v{SOCKETIO_VERSION}")
203
- self.logger.info(f"Enhanced validation: psutil {'available' if PSUTIL_AVAILABLE else 'not available'}")
204
- self.logger.info(f"File locking: {platform.system()} {'supported' if (platform.system() != 'Windows' or WINDOWS_LOCKING) else 'not supported'}")
205
- self.logger.info(f"Health monitoring: {'available' if HEALTH_MONITORING_AVAILABLE else 'not available'}")
206
-
207
- # Initialize health monitoring system
208
- self.health_monitor = None
209
- self.recovery_manager = None
210
- if HEALTH_MONITORING_AVAILABLE:
211
- self._initialize_health_monitoring()
212
-
213
- if self.process_start_time:
214
- self.logger.debug(f"Process start time: {self.process_start_time}")
215
-
216
- def _setup_logging(self) -> logging.Logger:
217
- """Setup dedicated logging for standalone server."""
218
- logger = logging.getLogger(f"socketio_standalone_{self.server_id}")
219
-
220
- if not logger.handlers:
221
- handler = logging.StreamHandler()
222
- formatter = logging.Formatter(
223
- f'%(asctime)s - StandaloneSocketIO[{self.server_id}] - %(levelname)s - %(message)s'
224
- )
225
- handler.setFormatter(formatter)
226
- logger.addHandler(handler)
227
- logger.setLevel(logging.INFO)
228
-
229
- return logger
230
-
231
- def _initialize_health_monitoring(self):
232
- """Initialize health monitoring and recovery systems."""
233
- try:
234
- # Health monitoring configuration
235
- health_config = {
236
- 'check_interval': 30, # Check every 30 seconds
237
- 'history_size': 100, # Keep 100 health check results
238
- 'aggregation_window': 300 # 5 minute aggregation window
239
- }
240
-
241
- self.health_monitor = AdvancedHealthMonitor(health_config)
242
-
243
- # Add health checkers
244
-
245
- # Process resource monitoring
246
- if PSUTIL_AVAILABLE:
247
- process_checker = ProcessResourceChecker(
248
- pid=self.pid,
249
- cpu_threshold=80.0, # 80% CPU threshold
250
- memory_threshold_mb=500, # 500MB memory threshold
251
- fd_threshold=1000 # 1000 file descriptor threshold
252
- )
253
- self.health_monitor.add_checker(process_checker)
254
-
255
- # Network connectivity monitoring
256
- network_checker = NetworkConnectivityChecker(
257
- host=self.host,
258
- port=self.port,
259
- timeout=2.0
260
- )
261
- self.health_monitor.add_checker(network_checker)
262
-
263
- # Service health monitoring (will be initialized after server stats are available)
264
- # This is added later in start_async after health_stats is fully initialized
265
-
266
- # Recovery manager configuration
267
- recovery_config = {
268
- 'enabled': True,
269
- 'check_interval': 60,
270
- 'max_recovery_attempts': 5,
271
- 'recovery_timeout': 30,
272
- 'circuit_breaker': {
273
- 'failure_threshold': 5,
274
- 'timeout_seconds': 300,
275
- 'success_threshold': 3
276
- },
277
- 'strategy': {
278
- 'warning_threshold': 2,
279
- 'critical_threshold': 1,
280
- 'failure_window_seconds': 300,
281
- 'min_recovery_interval': 60
282
- }
283
- }
284
-
285
- self.recovery_manager = RecoveryManager(recovery_config, self)
286
-
287
- # Link health monitor and recovery manager
288
- self.health_monitor.add_health_callback(self._handle_health_result)
289
-
290
- self.logger.info("Health monitoring and recovery systems initialized")
291
-
292
- except Exception as e:
293
- self.logger.error(f"Failed to initialize health monitoring: {e}")
294
- self.health_monitor = None
295
- self.recovery_manager = None
296
-
297
- def _handle_health_result(self, health_result: HealthCheckResult):
298
- """Handle health check results and trigger recovery if needed."""
299
- try:
300
- if self.recovery_manager:
301
- recovery_event = self.recovery_manager.handle_health_result(health_result)
302
- if recovery_event:
303
- self.logger.info(f"Recovery triggered: {recovery_event.action.value}")
304
- except Exception as e:
305
- self.logger.error(f"Error handling health result: {e}")
306
-
307
- # Enhanced error reporting for health check failures
308
- if ENHANCED_ERRORS_AVAILABLE:
309
- if hasattr(health_result, 'status') and health_result.status in ['critical', 'failed']:
310
- health_error = HealthCheckError(
311
- check_name=getattr(health_result, 'check_name', 'unknown'),
312
- check_status=getattr(health_result, 'status', 'failed'),
313
- check_details=getattr(health_result, 'details', {})
314
- )
315
- self.logger.error(f"\nHealth Check Failure Details:\n{health_error}")
316
-
317
- def _get_pidfile_path(self) -> Path:
318
- """Get path for PID file to track running server."""
319
- # Use system temp directory or user home
320
- if os.name == 'nt': # Windows
321
- temp_dir = Path(os.environ.get('TEMP', os.path.expanduser('~')))
322
- else: # Unix-like
323
- temp_dir = Path('/tmp') if Path('/tmp').exists() else Path.home()
324
-
325
- return temp_dir / f"claude_mpm_socketio_{self.port}.pid"
326
-
327
- def check_compatibility(self, client_version: str) -> Dict[str, Any]:
328
- """Check if client version is compatible with this server version.
329
-
330
- Returns compatibility info including warnings and supported features.
331
- """
332
- server_compat = COMPATIBILITY_MATRIX.get(self.server_version, {})
333
-
334
- result = {
335
- "compatible": False,
336
- "server_version": self.server_version,
337
- "client_version": client_version,
338
- "warnings": [],
339
- "supported_features": server_compat.get("features", []),
340
- "requirements": {
341
- "min_python": server_compat.get("min_python", "3.8"),
342
- "socketio_min": server_compat.get("socketio_min", "5.11.0")
343
- }
344
- }
345
-
346
- # Simple version compatibility check
347
- # In production, you'd use proper semantic versioning
348
- try:
349
- if client_version >= "0.7.0": # Minimum supported
350
- result["compatible"] = True
351
- else:
352
- result["warnings"].append(f"Client version {client_version} may not be fully supported")
353
- result["compatible"] = False
354
- except Exception as e:
355
- result["warnings"].append(f"Could not parse client version: {e}")
356
- result["compatible"] = False
357
-
358
- return result
359
-
360
- def _validate_process_identity(self, pid: int, expected_cmdline_patterns: List[str] = None) -> Dict[str, Any]:
361
- """Validate that a process is actually our Socket.IO server.
362
-
363
- Args:
364
- pid: Process ID to validate
365
- expected_cmdline_patterns: Command line patterns that should match our server
366
-
367
- Returns:
368
- Dict with validation results and process info
369
- """
370
- validation_result = {
371
- "is_valid": False,
372
- "is_zombie": False,
373
- "is_our_server": False,
374
- "process_info": {},
375
- "validation_errors": []
376
- }
377
-
378
- if not PSUTIL_AVAILABLE:
379
- validation_result["validation_errors"].append("psutil not available for enhanced validation")
380
- # Fallback to basic process existence check
381
- try:
382
- os.kill(pid, 0)
383
- validation_result["is_valid"] = True
384
- validation_result["process_info"] = {"pid": pid, "method": "basic_os_check"}
385
- except OSError:
386
- validation_result["validation_errors"].append(f"Process {pid} does not exist")
387
- return validation_result
388
-
389
- try:
390
- process = psutil.Process(pid)
391
-
392
- # Basic process info
393
- process_info = {
394
- "pid": pid,
395
- "status": process.status(),
396
- "create_time": process.create_time(),
397
- "name": process.name(),
398
- "cwd": None,
399
- "cmdline": [],
400
- "memory_info": None
401
- }
402
-
403
- # Check if process is zombie
404
- if process.status() == psutil.STATUS_ZOMBIE:
405
- validation_result["is_zombie"] = True
406
- validation_result["validation_errors"].append(f"Process {pid} is a zombie")
407
- validation_result["process_info"] = process_info
408
- return validation_result
409
-
410
- # Get additional process details
411
- try:
412
- process_info["cwd"] = process.cwd()
413
- process_info["cmdline"] = process.cmdline()
414
- process_info["memory_info"] = process.memory_info()._asdict()
415
- except (psutil.AccessDenied, psutil.NoSuchProcess) as e:
416
- validation_result["validation_errors"].append(f"Access denied getting process details: {e}")
417
-
418
- validation_result["process_info"] = process_info
419
- validation_result["is_valid"] = True
420
-
421
- # Validate this is likely our server process
422
- cmdline = process_info.get("cmdline", [])
423
- cmdline_str = " ".join(cmdline).lower()
424
-
425
- # Default patterns for our Socket.IO server
426
- if expected_cmdline_patterns is None:
427
- expected_cmdline_patterns = [
428
- "socketio",
429
- "standalone_socketio_server",
430
- "claude-mpm",
431
- str(self.port)
432
- ]
433
-
434
- # Check if any patterns match the command line
435
- matches = [pattern.lower() in cmdline_str for pattern in expected_cmdline_patterns]
436
- if any(matches):
437
- validation_result["is_our_server"] = True
438
- self.logger.debug(f"Process {pid} matches server patterns: {[p for p, m in zip(expected_cmdline_patterns, matches) if m]}")
439
- else:
440
- validation_result["validation_errors"].append(
441
- f"Process {pid} command line '{cmdline_str}' does not match expected patterns: {expected_cmdline_patterns}"
442
- )
443
- self.logger.warning(f"Process {pid} does not appear to be our server: {cmdline}")
444
-
445
- except psutil.NoSuchProcess:
446
- validation_result["validation_errors"].append(f"Process {pid} no longer exists")
447
- except psutil.AccessDenied as e:
448
- validation_result["validation_errors"].append(f"Access denied to process {pid}: {e}")
449
- except Exception as e:
450
- validation_result["validation_errors"].append(f"Error validating process {pid}: {e}")
451
-
452
- return validation_result
453
-
454
- def _acquire_pidfile_lock(self, pidfile_fd) -> bool:
455
- """Acquire exclusive lock on PID file.
456
-
457
- Args:
458
- pidfile_fd: Open file descriptor for PID file
459
-
460
- Returns:
461
- True if lock acquired successfully, False otherwise
462
- """
463
- try:
464
- if platform.system() == 'Windows' and WINDOWS_LOCKING:
465
- # Windows file locking
466
- msvcrt.locking(pidfile_fd.fileno(), msvcrt.LK_NBLCK, 1)
467
- return True
468
- else:
469
- # Unix file locking
470
- fcntl.flock(pidfile_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
471
- return True
472
- except (IOError, OSError) as e:
473
- self.logger.debug(f"Could not acquire PID file lock: {e}")
474
- return False
475
-
476
- def _release_pidfile_lock(self, pidfile_fd):
477
- """Release lock on PID file.
478
-
479
- Args:
480
- pidfile_fd: Open file descriptor for PID file
481
- """
482
- try:
483
- if platform.system() == 'Windows' and WINDOWS_LOCKING:
484
- msvcrt.locking(pidfile_fd.fileno(), msvcrt.LK_UNLCK, 1)
485
- else:
486
- fcntl.flock(pidfile_fd.fileno(), fcntl.LOCK_UN)
487
- except (IOError, OSError) as e:
488
- self.logger.debug(f"Error releasing PID file lock: {e}")
489
-
490
- def _validate_pidfile_timestamp(self, pidfile_path: Path, process_start_time: float) -> bool:
491
- """Validate that PID file was created around the same time as the process.
492
-
493
- Args:
494
- pidfile_path: Path to PID file
495
- process_start_time: Process start time from psutil
496
-
497
- Returns:
498
- True if timestamps are reasonably close, False otherwise
499
- """
500
- try:
501
- pidfile_mtime = pidfile_path.stat().st_mtime
502
- time_diff = abs(pidfile_mtime - process_start_time)
503
-
504
- # Allow up to 5 seconds difference (process start vs file creation)
505
- if time_diff <= 5.0:
506
- return True
507
- else:
508
- self.logger.warning(
509
- f"PID file timestamp ({pidfile_mtime}) and process start time ({process_start_time}) "
510
- f"differ by {time_diff:.2f} seconds"
511
- )
512
- return False
513
- except Exception as e:
514
- self.logger.warning(f"Could not validate PID file timestamp: {e}")
515
- return False
516
-
517
- def is_already_running(self, raise_on_conflict: bool = False) -> bool:
518
- """Enhanced check if another server instance is already running on this port.
519
-
520
- This method performs comprehensive validation including:
521
- - PID file existence and validity
522
- - Process identity verification (command line, start time)
523
- - Zombie process detection
524
- - Port availability check
525
- - Automatic cleanup of stale PID files
526
-
527
- Returns:
528
- True if a valid server is already running, False otherwise
529
- """
530
- self.logger.debug(f"Checking if server is already running on {self.host}:{self.port}")
531
-
532
- try:
533
- # Step 1: Check PID file existence
534
- if not self.pidfile_path.exists():
535
- self.logger.debug("No PID file found")
536
- return self._check_port_only(raise_on_conflict)
537
-
538
- self.logger.debug(f"Found PID file: {self.pidfile_path}")
539
-
540
- # Step 2: Read PID from file with support for both JSON and legacy formats
541
- try:
542
- with open(self.pidfile_path, 'r') as f:
543
- pid_content = f.read().strip()
544
-
545
- if not pid_content:
546
- self.logger.warning("Empty PID file")
547
- self._cleanup_stale_pidfile("empty_file")
548
- return self._check_port_only(raise_on_conflict)
549
-
550
- # Try JSON format first (new format)
551
- try:
552
- pidfile_data = json.loads(pid_content)
553
- old_pid = pidfile_data["pid"]
554
- server_id = pidfile_data.get("server_id", "unknown")
555
- self.logger.debug(f"Found PID {old_pid} for server {server_id} in JSON format")
556
- except (json.JSONDecodeError, KeyError, TypeError):
557
- # Fallback to legacy format (plain PID number)
558
- if pid_content.isdigit():
559
- old_pid = int(pid_content)
560
- self.logger.debug(f"Found PID {old_pid} in legacy format")
561
- else:
562
- self.logger.warning(f"Invalid PID content in file: '{pid_content[:100]}...' (truncated)")
563
- self._cleanup_stale_pidfile("invalid_content")
564
- return self._check_port_only(raise_on_conflict)
565
-
566
- except (IOError, ValueError) as e:
567
- self.logger.warning(f"Could not read PID file: {e}")
568
- self._cleanup_stale_pidfile("read_error")
569
- return self._check_port_only(raise_on_conflict)
570
-
571
- # Step 3: Enhanced process validation
572
- validation = self._validate_process_identity(old_pid)
573
-
574
- if not validation["is_valid"]:
575
- self.logger.info(f"Process {old_pid} is not valid: {validation['validation_errors']}")
576
- if raise_on_conflict and ENHANCED_ERRORS_AVAILABLE:
577
- raise StaleProcessError(
578
- pid=old_pid,
579
- pidfile_path=self.pidfile_path,
580
- process_status="not_found",
581
- validation_errors=validation['validation_errors']
582
- )
583
- self._cleanup_stale_pidfile("invalid_process")
584
- return self._check_port_only(raise_on_conflict)
585
-
586
- if validation["is_zombie"]:
587
- self.logger.info(f"Process {old_pid} is a zombie, cleaning up")
588
- if raise_on_conflict and ENHANCED_ERRORS_AVAILABLE:
589
- raise StaleProcessError(
590
- pid=old_pid,
591
- pidfile_path=self.pidfile_path,
592
- process_status="zombie",
593
- validation_errors=["Process is a zombie (terminated but not reaped)"]
594
- )
595
- self._cleanup_stale_pidfile("zombie_process")
596
- return self._check_port_only(raise_on_conflict)
597
-
598
- # Step 4: Verify this is actually our server process
599
- if not validation["is_our_server"]:
600
- self.logger.warning(
601
- f"Process {old_pid} exists but does not appear to be our Socket.IO server. "
602
- f"Command line: {validation['process_info'].get('cmdline', 'unknown')}"
603
- )
604
- # Don't automatically clean up - might be another legitimate process
605
- return self._check_port_only(raise_on_conflict)
606
-
607
- # Step 5: Validate process start time against PID file timestamp
608
- if PSUTIL_AVAILABLE and 'create_time' in validation['process_info']:
609
- process_start_time = validation['process_info']['create_time']
610
- if not self._validate_pidfile_timestamp(self.pidfile_path, process_start_time):
611
- self.logger.warning("PID file timestamp does not match process start time")
612
- # Continue anyway - timestamp validation is not critical
613
-
614
- # Step 6: All validations passed
615
- process_info = validation['process_info']
616
- self.logger.info(
617
- f"Found valid running server: PID {old_pid}, "
618
- f"status: {process_info.get('status', 'unknown')}, "
619
- f"name: {process_info.get('name', 'unknown')}"
620
- )
621
-
622
- if raise_on_conflict and ENHANCED_ERRORS_AVAILABLE:
623
- # Try to extract server ID from PID file if available
624
- server_id = "unknown"
625
- try:
626
- with open(self.pidfile_path, 'r') as f:
627
- content = f.read().strip()
628
- if content.startswith('{'):
629
- pidfile_data = json.loads(content)
630
- server_id = pidfile_data.get("server_id", "unknown")
631
- except:
632
- pass
633
-
634
- raise DaemonConflictError(
635
- port=self.port,
636
- existing_pid=old_pid,
637
- existing_server_id=server_id,
638
- process_info=process_info,
639
- pidfile_path=self.pidfile_path
640
- )
641
-
642
- return True
643
-
644
- except (DaemonConflictError, StaleProcessError, PortConflictError) as e:
645
- # Re-raise our enhanced errors instead of catching them
646
- raise
647
- except Exception as e:
648
- self.logger.error(f"Error during enhanced server check: {e}")
649
- # Fallback to basic port check on unexpected errors
650
- return self._check_port_only(raise_on_conflict)
651
-
652
- def _check_port_only(self, raise_on_conflict: bool = False) -> bool:
653
- """Fallback method to check if port is in use.
654
-
655
- Args:
656
- raise_on_conflict: If True, raises PortConflictError instead of returning True
657
-
658
- Returns:
659
- True if port is in use, False otherwise
660
-
661
- Raises:
662
- PortConflictError: If raise_on_conflict=True and port is in use
663
- """
664
- try:
665
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
666
- s.settimeout(1.0)
667
- result = s.connect_ex((self.host, self.port))
668
- if result == 0:
669
- self.logger.info(f"Port {self.port} is in use by some process")
670
-
671
- if raise_on_conflict and ENHANCED_ERRORS_AVAILABLE:
672
- # Try to identify the conflicting process if psutil is available
673
- conflicting_process = {}
674
- if PSUTIL_AVAILABLE:
675
- try:
676
- import psutil
677
- for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
678
- try:
679
- for conn in proc.connections():
680
- if (conn.laddr.ip == self.host or conn.laddr.ip == '0.0.0.0') and conn.laddr.port == self.port:
681
- conflicting_process = {
682
- 'pid': proc.info['pid'],
683
- 'name': proc.info['name'],
684
- 'cmdline': proc.info['cmdline']
685
- }
686
- break
687
- except (psutil.NoSuchProcess, psutil.AccessDenied):
688
- continue
689
- if conflicting_process:
690
- break
691
- except Exception:
692
- pass # Ignore errors in process discovery
693
-
694
- raise PortConflictError(
695
- port=self.port,
696
- host=self.host,
697
- conflicting_process=conflicting_process
698
- )
699
-
700
- return True
701
- except Exception as e:
702
- if not isinstance(e, PortConflictError): # Don't mask our own exceptions
703
- self.logger.debug(f"Error checking port availability: {e}")
704
-
705
- return False
706
-
707
- def _cleanup_stale_pidfile(self, reason: str):
708
- """Clean up stale PID file with logging.
709
-
710
- Args:
711
- reason: Reason for cleanup (for logging)
712
- """
713
- try:
714
- if self.pidfile_path.exists():
715
- self.pidfile_path.unlink()
716
- self.logger.info(f"Cleaned up stale PID file (reason: {reason}): {self.pidfile_path}")
717
- except Exception as e:
718
- self.logger.error(f"Failed to clean up stale PID file: {e}")
719
-
720
- def create_pidfile(self):
721
- """Create PID file with exclusive locking to track this server instance.
722
-
723
- This method creates a PID file with exclusive locking to prevent race conditions
724
- and ensures only one server instance can hold the lock at a time.
725
- """
726
- try:
727
- self.pidfile_path.parent.mkdir(parents=True, exist_ok=True)
728
-
729
- # Open file for writing with exclusive creation
730
- pidfile_fd = open(self.pidfile_path, 'w')
731
-
732
- # Try to acquire exclusive lock
733
- if not self._acquire_pidfile_lock(pidfile_fd):
734
- pidfile_fd.close()
735
- if ENHANCED_ERRORS_AVAILABLE:
736
- raise DaemonConflictError(
737
- port=self.port,
738
- existing_pid=0, # Unknown PID since we can't get lock
739
- existing_server_id="unknown",
740
- pidfile_path=self.pidfile_path
741
- )
742
- else:
743
- raise RuntimeError("Could not acquire exclusive lock on PID file")
744
-
745
- # Write PID and additional metadata
746
- pidfile_content = {
747
- "pid": self.pid,
748
- "server_id": self.server_id,
749
- "server_version": self.server_version,
750
- "port": self.port,
751
- "host": self.host,
752
- "start_time": self.start_time.isoformat() + "Z",
753
- "process_start_time": self.process_start_time if self.process_start_time else None,
754
- "python_version": sys.version.split()[0],
755
- "platform": platform.system(),
756
- "created_at": datetime.utcnow().isoformat() + "Z"
757
- }
758
-
759
- # Write JSON format for better validation
760
- pidfile_fd.write(json.dumps(pidfile_content, indent=2))
761
- pidfile_fd.flush()
762
-
763
- # Keep file descriptor open to maintain lock
764
- self.pidfile_lock = pidfile_fd
765
-
766
- self.logger.info(f"Created PID file with exclusive lock: {self.pidfile_path}")
767
- self.logger.debug(f"PID file content: {pidfile_content}")
768
-
769
- except Exception as e:
770
- self.logger.error(f"Failed to create PID file: {e}")
771
- if 'pidfile_fd' in locals():
772
- try:
773
- pidfile_fd.close()
774
- except:
775
- pass
776
- raise
777
-
778
- def remove_pidfile(self):
779
- """Remove PID file and release lock on shutdown."""
780
- try:
781
- # Release file lock first
782
- if self.pidfile_lock:
783
- try:
784
- self._release_pidfile_lock(self.pidfile_lock)
785
- self.pidfile_lock.close()
786
- self.pidfile_lock = None
787
- self.logger.debug("Released PID file lock")
788
- except Exception as e:
789
- self.logger.warning(f"Error releasing PID file lock: {e}")
790
-
791
- # Remove PID file
792
- if self.pidfile_path.exists():
793
- self.pidfile_path.unlink()
794
- self.logger.info(f"Removed PID file: {self.pidfile_path}")
795
-
796
- except Exception as e:
797
- self.logger.error(f"Failed to remove PID file: {e}")
798
-
799
- def setup_signal_handlers(self):
800
- """Setup signal handlers for graceful shutdown."""
801
- def signal_handler(signum, frame):
802
- self.logger.info(f"Received signal {signum}, initiating shutdown...")
803
- self.stop()
804
- sys.exit(0)
805
-
806
- signal.signal(signal.SIGINT, signal_handler)
807
- signal.signal(signal.SIGTERM, signal_handler)
808
-
809
- if hasattr(signal, 'SIGHUP'):
810
- signal.signal(signal.SIGHUP, signal_handler)
811
-
812
- async def start_async(self):
813
- """Start the server asynchronously."""
814
- if not SOCKETIO_AVAILABLE:
815
- error_msg = "Socket.IO dependencies not available. Install with: pip install python-socketio aiohttp"
816
- if ENHANCED_ERRORS_AVAILABLE:
817
- raise RuntimeError(error_msg + "\n\nInstallation steps:\n 1. pip install python-socketio aiohttp\n 2. Restart the server\n 3. Verify installation: python -c 'import socketio; print(socketio.__version__)'")
818
- else:
819
- raise RuntimeError(error_msg)
820
-
821
- self.logger.info(f"Starting standalone Socket.IO server v{self.server_version}")
822
-
823
- # Create Socket.IO server with production settings
824
- self.sio = socketio.AsyncServer(
825
- cors_allowed_origins="*", # Configure appropriately for production
826
- async_mode='aiohttp',
827
- ping_timeout=60,
828
- ping_interval=25,
829
- max_http_buffer_size=1000000,
830
- logger=False, # Use our own logger
831
- engineio_logger=False
832
- )
833
-
834
- # Create aiohttp application
835
- self.app = web.Application()
836
- self.sio.attach(self.app)
837
-
838
- # Setup routes and event handlers
839
- self._setup_routes()
840
- self._setup_event_handlers()
841
-
842
- # Start the server
843
- try:
844
- self.runner = web.AppRunner(self.app)
845
- await self.runner.setup()
846
-
847
- self.site = web.TCPSite(self.runner, self.host, self.port)
848
- await self.site.start()
849
-
850
- self.running = True
851
-
852
- # Create PID file after successful server start
853
- self.create_pidfile()
854
-
855
- # Start health monitoring
856
- if self.health_monitor:
857
- # Add service health checker now that stats are available
858
- service_checker = ServiceHealthChecker(
859
- service_stats=self.health_stats,
860
- max_clients=1000,
861
- max_error_rate=0.1
862
- )
863
- self.health_monitor.add_checker(service_checker)
864
-
865
- # Start monitoring
866
- self.health_monitor.start_monitoring()
867
- self.logger.info("Health monitoring started")
868
-
869
- except Exception as e:
870
- self.logger.error(f"Failed to start server: {e}")
871
- # Clean up partial initialization
872
- if hasattr(self, 'runner') and self.runner:
873
- try:
874
- await self.runner.cleanup()
875
- except:
876
- pass
877
-
878
- # Enhanced error handling for common startup failures
879
- if ENHANCED_ERRORS_AVAILABLE:
880
- if "Address already in use" in str(e) or "Permission denied" in str(e):
881
- # This is likely a port conflict
882
- try:
883
- # Check if port is in use and raise appropriate error
884
- self._check_port_only(raise_on_conflict=True)
885
- except PortConflictError:
886
- # Re-raise the more specific error
887
- raise
888
-
889
- raise
890
-
891
- self.logger.info(f"🚀 Standalone Socket.IO server STARTED on http://{self.host}:{self.port}")
892
- self.logger.info(f"🔧 Server ID: {self.server_id}")
893
- self.logger.info(f"💾 PID file: {self.pidfile_path}")
894
-
895
- def start(self):
896
- """Start the server in the main thread (for standalone execution)."""
897
- if self.is_already_running():
898
- self.logger.error("Server is already running. Use stop() first or choose a different port.")
899
- return False
900
-
901
- self.setup_signal_handlers()
902
-
903
- # Run in main thread for standalone operation
904
- try:
905
- self.loop = asyncio.new_event_loop()
906
- asyncio.set_event_loop(self.loop)
907
- self.loop.run_until_complete(self._run_forever())
908
- except KeyboardInterrupt:
909
- self.logger.info("Received KeyboardInterrupt, shutting down...")
910
- except Exception as e:
911
- self.logger.error(f"Server error: {e}")
912
- raise
913
- finally:
914
- self.stop()
915
-
916
- return True
917
-
918
- async def _run_forever(self):
919
- """Run the server until stopped."""
920
- await self.start_async()
921
-
922
- try:
923
- # Keep server running with periodic health checks
924
- last_health_check = time.time()
925
-
926
- while self.running:
927
- await asyncio.sleep(1)
928
-
929
- # Periodic health check and stats update
930
- now = time.time()
931
- if now - last_health_check > 30: # Every 30 seconds
932
- self._update_health_stats()
933
- last_health_check = now
934
-
935
- except Exception as e:
936
- self.logger.error(f"Error in server loop: {e}")
937
- raise
938
-
939
- def stop(self):
940
- """Stop the server gracefully."""
941
- self.logger.info("Stopping standalone Socket.IO server...")
942
- self.running = False
943
-
944
- if self.loop and self.loop.is_running():
945
- # Schedule shutdown in the event loop
946
- self.loop.create_task(self._shutdown_async())
947
- else:
948
- # Direct shutdown
949
- asyncio.run(self._shutdown_async())
950
-
951
- self.remove_pidfile()
952
- self.logger.info("Server stopped")
953
-
954
- async def _shutdown_async(self):
955
- """Async shutdown process."""
956
- try:
957
- # Stop health monitoring
958
- if self.health_monitor:
959
- await self.health_monitor.stop_monitoring()
960
- self.logger.info("Health monitoring stopped")
961
-
962
- # Close all client connections
963
- if self.sio:
964
- await self.sio.shutdown()
965
-
966
- # Stop the web server
967
- if self.site:
968
- await self.site.stop()
969
- if self.runner:
970
- await self.runner.cleanup()
971
-
972
- except Exception as e:
973
- self.logger.error(f"Error during shutdown: {e}")
974
-
975
- def _setup_routes(self):
976
- """Setup HTTP routes for health checks and admin endpoints."""
977
-
978
- async def version_endpoint(request):
979
- """Version discovery endpoint."""
980
- compatibility_info = {
981
- "server_version": self.server_version,
982
- "server_id": self.server_id,
983
- "socketio_version": SOCKETIO_VERSION,
984
- "compatibility_matrix": COMPATIBILITY_MATRIX,
985
- "supported_client_versions": COMPATIBILITY_MATRIX[self.server_version].get("claude_mpm_versions", []),
986
- "features": COMPATIBILITY_MATRIX[self.server_version].get("features", [])
987
- }
988
- return web.json_response(compatibility_info)
989
-
990
- async def health_endpoint(request):
991
- """Health check endpoint with detailed diagnostics."""
992
- uptime = (datetime.utcnow() - self.start_time).total_seconds()
993
-
994
- health_info = {
995
- "status": "healthy" if self.running else "stopped",
996
- "server_version": self.server_version,
997
- "server_id": self.server_id,
998
- "pid": self.pid,
999
- "uptime_seconds": uptime,
1000
- "start_time": self.start_time.isoformat() + "Z",
1001
- "timestamp": datetime.utcnow().isoformat() + "Z",
1002
- "clients_connected": len(self.clients),
1003
- "client_versions": dict(self.client_versions),
1004
- "health_stats": dict(self.health_stats),
1005
- "port": self.port,
1006
- "host": self.host,
1007
- "dependencies": {
1008
- "socketio_version": SOCKETIO_VERSION,
1009
- "python_version": sys.version.split()[0]
1010
- }
1011
- }
1012
- return web.json_response(health_info)
1013
-
1014
- async def compatibility_check(request):
1015
- """Check compatibility with a specific client version."""
1016
- data = await request.json()
1017
- client_version = data.get("client_version", "unknown")
1018
-
1019
- compatibility = self.check_compatibility(client_version)
1020
- return web.json_response(compatibility)
1021
-
1022
- async def stats_endpoint(request):
1023
- """Server statistics endpoint."""
1024
- stats = {
1025
- "server_info": {
1026
- "version": self.server_version,
1027
- "id": self.server_id,
1028
- "uptime": (datetime.utcnow() - self.start_time).total_seconds()
1029
- },
1030
- "connections": {
1031
- "current_clients": len(self.clients),
1032
- "total_served": self.health_stats["clients_served"],
1033
- "client_versions": dict(self.client_versions)
1034
- },
1035
- "events": {
1036
- "total_processed": self.health_stats["events_processed"],
1037
- "history_size": len(self.event_history),
1038
- "last_activity": self.health_stats["last_activity"]
1039
- },
1040
- "errors": self.health_stats["errors"]
1041
- }
1042
- return web.json_response(stats)
1043
-
1044
- # Register routes
1045
- self.app.router.add_get('/version', version_endpoint)
1046
- self.app.router.add_get('/health', health_endpoint)
1047
- self.app.router.add_get('/status', health_endpoint) # Alias
1048
- self.app.router.add_post('/compatibility', compatibility_check)
1049
- self.app.router.add_get('/stats', stats_endpoint)
1050
-
1051
- # Serve Socket.IO client library
1052
- self.app.router.add_static('/socket.io/',
1053
- path=Path(__file__).parent / 'static',
1054
- name='socketio_static')
1055
-
1056
- def _setup_event_handlers(self):
1057
- """Setup Socket.IO event handlers."""
1058
-
1059
- @self.sio.event
1060
- async def connect(sid, environ, auth):
1061
- """Handle client connection with version compatibility checking."""
1062
- self.clients.add(sid)
1063
- client_addr = environ.get('REMOTE_ADDR', 'unknown')
1064
-
1065
- # Extract client version from auth if provided
1066
- client_version = "unknown"
1067
- if auth and isinstance(auth, dict):
1068
- client_version = auth.get('claude_mpm_version', 'unknown')
1069
-
1070
- self.client_versions[sid] = client_version
1071
- self.health_stats["clients_served"] += 1
1072
- self.health_stats["last_activity"] = datetime.utcnow().isoformat() + "Z"
1073
-
1074
- self.logger.info(f"🔗 Client {sid} connected from {client_addr}")
1075
- self.logger.info(f"📋 Client version: {client_version}")
1076
- self.logger.info(f"📊 Total clients: {len(self.clients)}")
1077
-
1078
- # Check version compatibility
1079
- compatibility = self.check_compatibility(client_version)
1080
-
1081
- # Send connection acknowledgment with compatibility info
1082
- await self.sio.emit('connection_ack', {
1083
- "server_version": self.server_version,
1084
- "server_id": self.server_id,
1085
- "compatibility": compatibility,
1086
- "timestamp": datetime.utcnow().isoformat() + "Z"
1087
- }, room=sid)
1088
-
1089
- # Send current server status
1090
- await self._send_server_status(sid)
1091
-
1092
- if not compatibility["compatible"]:
1093
- self.logger.warning(f"⚠️ Client {sid} version {client_version} has compatibility issues")
1094
- await self.sio.emit('compatibility_warning', compatibility, room=sid)
1095
-
1096
- @self.sio.event
1097
- async def disconnect(sid):
1098
- """Handle client disconnection."""
1099
- if sid in self.clients:
1100
- self.clients.remove(sid)
1101
- if sid in self.client_versions:
1102
- del self.client_versions[sid]
1103
-
1104
- self.logger.info(f"🔌 Client {sid} disconnected")
1105
- self.logger.info(f"📊 Remaining clients: {len(self.clients)}")
1106
-
1107
- @self.sio.event
1108
- async def ping(sid, data=None):
1109
- """Handle ping requests."""
1110
- await self.sio.emit('pong', {
1111
- "timestamp": datetime.utcnow().isoformat() + "Z",
1112
- "server_id": self.server_id
1113
- }, room=sid)
1114
-
1115
- @self.sio.event
1116
- async def get_version(sid):
1117
- """Handle version info requests."""
1118
- version_info = {
1119
- "server_version": self.server_version,
1120
- "server_id": self.server_id,
1121
- "socketio_version": SOCKETIO_VERSION,
1122
- "compatibility_matrix": COMPATIBILITY_MATRIX
1123
- }
1124
- await self.sio.emit('version_info', version_info, room=sid)
1125
-
1126
- @self.sio.event
1127
- async def claude_event(sid, data):
1128
- """Handle events from claude-mpm clients and broadcast to other clients."""
1129
- try:
1130
- # Add server metadata
1131
- enhanced_data = {
1132
- **data,
1133
- "server_id": self.server_id,
1134
- "received_at": datetime.utcnow().isoformat() + "Z"
1135
- }
1136
-
1137
- # Store in event history
1138
- self.event_history.append(enhanced_data)
1139
- self.health_stats["events_processed"] += 1
1140
- self.health_stats["last_activity"] = datetime.utcnow().isoformat() + "Z"
1141
-
1142
- # Broadcast to all other clients
1143
- await self.sio.emit('claude_event', enhanced_data, skip_sid=sid)
1144
-
1145
- self.logger.debug(f"📤 Broadcasted claude_event from {sid} to {len(self.clients)-1} clients")
1146
-
1147
- except Exception as e:
1148
- self.logger.error(f"Error handling claude_event: {e}")
1149
- self.health_stats["errors"] += 1
1150
-
1151
- # Check if error rate is becoming concerning
1152
- if ENHANCED_ERRORS_AVAILABLE and self.health_stats["errors"] > 0:
1153
- error_rate = self.health_stats["errors"] / max(self.health_stats["events_processed"], 1)
1154
- if error_rate > 0.1: # More than 10% error rate
1155
- self.logger.warning(f"⚠️ High error rate detected: {error_rate:.2%} ({self.health_stats['errors']} errors out of {self.health_stats['events_processed']} events)")
1156
-
1157
- @self.sio.event
1158
- async def get_history(sid, data=None):
1159
- """Handle event history requests."""
1160
- params = data or {}
1161
- limit = min(params.get("limit", 100), len(self.event_history))
1162
-
1163
- history = list(self.event_history)[-limit:] if limit > 0 else []
1164
-
1165
- await self.sio.emit('event_history', {
1166
- "events": history,
1167
- "total_available": len(self.event_history),
1168
- "returned": len(history)
1169
- }, room=sid)
1170
-
1171
- async def _send_server_status(self, sid: str):
1172
- """Send current server status to a client."""
1173
- status = {
1174
- "server_version": self.server_version,
1175
- "server_id": self.server_id,
1176
- "uptime": (datetime.utcnow() - self.start_time).total_seconds(),
1177
- "clients_connected": len(self.clients),
1178
- "events_processed": self.health_stats["events_processed"],
1179
- "timestamp": datetime.utcnow().isoformat() + "Z"
1180
- }
1181
- await self.sio.emit('server_status', status, room=sid)
1182
-
1183
- def _update_health_stats(self):
1184
- """Update health statistics."""
1185
- self.logger.debug(f"🏥 Health check - Clients: {len(self.clients)}, "
1186
- f"Events: {self.health_stats['events_processed']}, "
1187
- f"Errors: {self.health_stats['errors']}")
1188
-
1189
-
1190
- def main():
1191
- """Main entry point for standalone server execution."""
1192
- import argparse
1193
- import json
1194
- import time
1195
-
1196
- parser = argparse.ArgumentParser(description="Standalone Claude MPM Socket.IO Server")
1197
- parser.add_argument("--host", default="localhost", help="Host to bind to")
1198
- parser.add_argument("--port", type=int, default=8765, help="Port to bind to")
1199
- parser.add_argument("--server-id", help="Custom server ID")
1200
- parser.add_argument("--check-running", action="store_true",
1201
- help="Check if server is already running and exit")
1202
- parser.add_argument("--stop", action="store_true", help="Stop running server")
1203
- parser.add_argument("--version", action="store_true", help="Show version info")
1204
-
1205
- args = parser.parse_args()
1206
-
1207
- if args.version:
1208
- print(f"Standalone Socket.IO Server v{STANDALONE_SERVER_VERSION}")
1209
- print(f"Socket.IO v{SOCKETIO_VERSION}")
1210
- print(f"Compatibility: {COMPATIBILITY_MATRIX[STANDALONE_SERVER_VERSION]['claude_mpm_versions']}")
1211
- return
1212
-
1213
- server = StandaloneSocketIOServer(
1214
- host=args.host,
1215
- port=args.port,
1216
- server_id=args.server_id
1217
- )
1218
-
1219
- if args.check_running:
1220
- if server.is_already_running():
1221
- print(f"Server is running on {args.host}:{args.port}")
1222
- sys.exit(0)
1223
- else:
1224
- print(f"No server running on {args.host}:{args.port}")
1225
- sys.exit(1)
1226
-
1227
- if args.stop:
1228
- if server.is_already_running():
1229
- # Send termination signal to running server with enhanced validation
1230
- try:
1231
- # Read and validate PID file
1232
- with open(server.pidfile_path, 'r') as f:
1233
- content = f.read().strip()
1234
-
1235
- # Try to parse as JSON first (new format), fallback to plain PID
1236
- try:
1237
- pidfile_data = json.loads(content)
1238
- pid = pidfile_data["pid"]
1239
- server_id = pidfile_data.get("server_id", "unknown")
1240
- print(f"Found server {server_id} with PID {pid}")
1241
- except (json.JSONDecodeError, KeyError):
1242
- # Fallback to old format
1243
- pid = int(content)
1244
- server_id = "unknown"
1245
-
1246
- # Validate the process before attempting to stop it
1247
- validation = server._validate_process_identity(pid)
1248
- if not validation["is_valid"]:
1249
- print(f"Process {pid} is not valid or no longer exists")
1250
- server._cleanup_stale_pidfile("stop_command_invalid_process")
1251
- print("Cleaned up stale PID file")
1252
- sys.exit(1)
1253
-
1254
- if validation["is_zombie"]:
1255
- print(f"Process {pid} is a zombie, cleaning up PID file")
1256
- server._cleanup_stale_pidfile("stop_command_zombie")
1257
- sys.exit(0)
1258
-
1259
- if not validation["is_our_server"]:
1260
- print(f"Warning: Process {pid} may not be our Socket.IO server")
1261
- print(f"Command line: {validation['process_info'].get('cmdline', 'unknown')}")
1262
- response = input("Stop it anyway? [y/N]: ")
1263
- if response.lower() != 'y':
1264
- print("Aborted")
1265
- sys.exit(1)
1266
-
1267
- # Send termination signal
1268
- os.kill(pid, signal.SIGTERM)
1269
- print(f"Sent stop signal to server (PID: {pid})")
1270
-
1271
- # Wait a moment for graceful shutdown
1272
- time.sleep(2)
1273
-
1274
- # Check if process is still running
1275
- try:
1276
- os.kill(pid, 0)
1277
- print(f"Server is still running, sending SIGKILL...")
1278
- os.kill(pid, signal.SIGKILL)
1279
- time.sleep(1)
1280
- except OSError:
1281
- print("Server stopped successfully")
1282
-
1283
- except Exception as e:
1284
- print(f"Error stopping server: {e}")
1285
- sys.exit(1)
1286
- else:
1287
- print("No server running to stop")
1288
- sys.exit(1)
1289
- return
1290
-
1291
- # Start the server
1292
- try:
1293
- server.start()
1294
- except Exception as e:
1295
- print(f"Failed to start server: {e}")
1296
- sys.exit(1)
1297
-
1298
-
1299
- if __name__ == "__main__":
1300
- main()