claude-mpm 3.9.9__py3-none-any.whl → 4.0.3__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 (411) 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 +155 -0
  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 +1 -1
  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 +90 -49
  31. claude_mpm/cli/__main__.py +3 -2
  32. claude_mpm/cli/commands/__init__.py +21 -18
  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 +143 -762
  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 +203 -81
  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 +305 -197
  50. claude_mpm/cli/parser.py +24 -1150
  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 +104 -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 +217 -0
  69. claude_mpm/config/paths.py +94 -208
  70. claude_mpm/config/socketio_config.py +84 -73
  71. claude_mpm/constants.py +36 -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/css/dashboard.css +27 -8
  113. claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
  114. claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
  115. claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
  116. claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
  117. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
  118. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
  119. claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
  120. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
  121. claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
  122. claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
  123. claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
  124. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
  125. claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
  126. claude_mpm/dashboard/static/dist/dashboard.js +2 -0
  127. claude_mpm/dashboard/static/dist/socket-client.js +2 -0
  128. claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
  129. claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
  130. claude_mpm/dashboard/static/js/components/event-viewer.js +74 -70
  131. claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
  132. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +106 -92
  133. claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
  134. claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
  135. claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
  136. claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
  137. claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
  138. claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
  139. claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
  140. claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
  141. claude_mpm/dashboard/static/js/dashboard.js +178 -453
  142. claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
  143. claude_mpm/dashboard/static/js/socket-client.js +120 -54
  144. claude_mpm/dashboard/templates/index.html +40 -50
  145. claude_mpm/experimental/cli_enhancements.py +60 -58
  146. claude_mpm/generators/__init__.py +1 -1
  147. claude_mpm/generators/agent_profile_generator.py +75 -65
  148. claude_mpm/hooks/__init__.py +1 -1
  149. claude_mpm/hooks/base_hook.py +33 -28
  150. claude_mpm/hooks/claude_hooks/__init__.py +1 -1
  151. claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
  152. claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
  153. claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
  154. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
  155. claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
  156. claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
  157. claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
  158. claude_mpm/hooks/memory_integration_hook.py +140 -100
  159. claude_mpm/hooks/tool_call_interceptor.py +89 -76
  160. claude_mpm/hooks/validation_hooks.py +57 -49
  161. claude_mpm/init.py +145 -121
  162. claude_mpm/models/__init__.py +9 -9
  163. claude_mpm/models/agent_definition.py +33 -23
  164. claude_mpm/models/agent_session.py +228 -200
  165. claude_mpm/scripts/__init__.py +1 -1
  166. claude_mpm/scripts/socketio_daemon.py +192 -75
  167. claude_mpm/scripts/socketio_server_manager.py +328 -0
  168. claude_mpm/scripts/start_activity_logging.py +25 -22
  169. claude_mpm/services/__init__.py +68 -43
  170. claude_mpm/services/agent_capabilities_service.py +271 -0
  171. claude_mpm/services/agents/__init__.py +23 -32
  172. claude_mpm/services/agents/deployment/__init__.py +3 -3
  173. claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
  174. claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
  175. claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
  176. claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
  177. claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
  178. claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
  179. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
  180. claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
  181. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
  182. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
  183. claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
  184. claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
  185. claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
  186. claude_mpm/services/agents/deployment/agent_validator.py +352 -0
  187. claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
  188. claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
  189. claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
  190. claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
  191. claude_mpm/services/agents/deployment/config/__init__.py +13 -0
  192. claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
  193. claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
  194. claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
  195. claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
  196. claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
  197. claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
  198. claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
  199. claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
  200. claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
  201. claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
  202. claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
  203. claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
  204. claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
  205. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
  206. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
  207. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
  208. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
  209. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
  210. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
  211. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
  212. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
  213. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
  214. claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
  215. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
  216. claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
  217. claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
  218. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
  219. claude_mpm/services/agents/deployment/results/__init__.py +13 -0
  220. claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
  221. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
  222. claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
  223. claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
  224. claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
  225. claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
  226. claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
  227. claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
  228. claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
  229. claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
  230. claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
  231. claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
  232. claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
  233. claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
  234. claude_mpm/services/agents/loading/__init__.py +2 -2
  235. claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
  236. claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
  237. claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
  238. claude_mpm/services/agents/management/__init__.py +2 -2
  239. claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
  240. claude_mpm/services/agents/management/agent_management_service.py +209 -156
  241. claude_mpm/services/agents/memory/__init__.py +9 -6
  242. claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
  243. claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
  244. claude_mpm/services/agents/memory/analyzer.py +430 -0
  245. claude_mpm/services/agents/memory/content_manager.py +376 -0
  246. claude_mpm/services/agents/memory/template_generator.py +468 -0
  247. claude_mpm/services/agents/registry/__init__.py +7 -10
  248. claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
  249. claude_mpm/services/agents/registry/modification_tracker.py +351 -285
  250. claude_mpm/services/async_session_logger.py +187 -153
  251. claude_mpm/services/claude_session_logger.py +87 -72
  252. claude_mpm/services/command_handler_service.py +217 -0
  253. claude_mpm/services/communication/__init__.py +3 -2
  254. claude_mpm/services/core/__init__.py +50 -97
  255. claude_mpm/services/core/base.py +60 -53
  256. claude_mpm/services/core/interfaces/__init__.py +188 -0
  257. claude_mpm/services/core/interfaces/agent.py +351 -0
  258. claude_mpm/services/core/interfaces/communication.py +343 -0
  259. claude_mpm/services/core/interfaces/infrastructure.py +413 -0
  260. claude_mpm/services/core/interfaces/service.py +434 -0
  261. claude_mpm/services/core/interfaces.py +19 -944
  262. claude_mpm/services/event_aggregator.py +208 -170
  263. claude_mpm/services/exceptions.py +387 -308
  264. claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
  265. claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
  266. claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
  267. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
  268. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
  269. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
  270. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
  271. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
  272. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
  273. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
  274. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
  275. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
  276. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
  277. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
  278. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
  279. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
  280. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
  281. claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
  283. claude_mpm/services/hook_service.py +106 -114
  284. claude_mpm/services/infrastructure/__init__.py +7 -5
  285. claude_mpm/services/infrastructure/context_preservation.py +571 -0
  286. claude_mpm/services/infrastructure/daemon_manager.py +279 -0
  287. claude_mpm/services/infrastructure/logging.py +83 -76
  288. claude_mpm/services/infrastructure/monitoring.py +547 -404
  289. claude_mpm/services/mcp_gateway/__init__.py +40 -23
  290. claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
  291. claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
  292. claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
  293. claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
  294. claude_mpm/services/mcp_gateway/core/__init__.py +14 -21
  295. claude_mpm/services/mcp_gateway/core/base.py +80 -67
  296. claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
  297. claude_mpm/services/mcp_gateway/core/interfaces.py +97 -93
  298. claude_mpm/services/mcp_gateway/main.py +307 -127
  299. claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +100 -101
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
  302. claude_mpm/services/mcp_gateway/server/__init__.py +4 -4
  303. claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +149 -153
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +110 -121
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
  309. claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
  310. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
  311. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
  312. claude_mpm/services/memory/__init__.py +2 -2
  313. claude_mpm/services/memory/builder.py +451 -362
  314. claude_mpm/services/memory/cache/__init__.py +2 -2
  315. claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
  316. claude_mpm/services/memory/cache/simple_cache.py +107 -93
  317. claude_mpm/services/memory/indexed_memory.py +195 -193
  318. claude_mpm/services/memory/optimizer.py +267 -234
  319. claude_mpm/services/memory/router.py +571 -263
  320. claude_mpm/services/memory_hook_service.py +237 -0
  321. claude_mpm/services/port_manager.py +223 -0
  322. claude_mpm/services/project/__init__.py +3 -3
  323. claude_mpm/services/project/analyzer.py +451 -305
  324. claude_mpm/services/project/registry.py +262 -240
  325. claude_mpm/services/recovery_manager.py +287 -231
  326. claude_mpm/services/response_tracker.py +87 -67
  327. claude_mpm/services/runner_configuration_service.py +587 -0
  328. claude_mpm/services/session_management_service.py +304 -0
  329. claude_mpm/services/socketio/__init__.py +4 -4
  330. claude_mpm/services/socketio/client_proxy.py +174 -0
  331. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  332. claude_mpm/services/socketio/handlers/base.py +44 -30
  333. claude_mpm/services/socketio/handlers/connection.py +145 -65
  334. claude_mpm/services/socketio/handlers/file.py +123 -108
  335. claude_mpm/services/socketio/handlers/git.py +607 -373
  336. claude_mpm/services/socketio/handlers/hook.py +170 -0
  337. claude_mpm/services/socketio/handlers/memory.py +4 -4
  338. claude_mpm/services/socketio/handlers/project.py +4 -4
  339. claude_mpm/services/socketio/handlers/registry.py +53 -38
  340. claude_mpm/services/socketio/server/__init__.py +18 -0
  341. claude_mpm/services/socketio/server/broadcaster.py +252 -0
  342. claude_mpm/services/socketio/server/core.py +399 -0
  343. claude_mpm/services/socketio/server/main.py +323 -0
  344. claude_mpm/services/socketio_client_manager.py +160 -133
  345. claude_mpm/services/socketio_server.py +36 -1885
  346. claude_mpm/services/subprocess_launcher_service.py +316 -0
  347. claude_mpm/services/system_instructions_service.py +258 -0
  348. claude_mpm/services/ticket_manager.py +20 -534
  349. claude_mpm/services/utility_service.py +285 -0
  350. claude_mpm/services/version_control/__init__.py +18 -21
  351. claude_mpm/services/version_control/branch_strategy.py +20 -10
  352. claude_mpm/services/version_control/conflict_resolution.py +37 -13
  353. claude_mpm/services/version_control/git_operations.py +52 -21
  354. claude_mpm/services/version_control/semantic_versioning.py +92 -53
  355. claude_mpm/services/version_control/version_parser.py +145 -125
  356. claude_mpm/services/version_service.py +270 -0
  357. claude_mpm/storage/__init__.py +9 -0
  358. claude_mpm/storage/state_storage.py +552 -0
  359. claude_mpm/ticket_wrapper.py +2 -2
  360. claude_mpm/utils/__init__.py +2 -2
  361. claude_mpm/utils/agent_dependency_loader.py +453 -243
  362. claude_mpm/utils/config_manager.py +157 -118
  363. claude_mpm/utils/console.py +1 -1
  364. claude_mpm/utils/dependency_cache.py +102 -107
  365. claude_mpm/utils/dependency_manager.py +52 -47
  366. claude_mpm/utils/dependency_strategies.py +131 -96
  367. claude_mpm/utils/environment_context.py +110 -102
  368. claude_mpm/utils/error_handler.py +75 -55
  369. claude_mpm/utils/file_utils.py +80 -67
  370. claude_mpm/utils/framework_detection.py +12 -11
  371. claude_mpm/utils/import_migration_example.py +12 -60
  372. claude_mpm/utils/imports.py +48 -45
  373. claude_mpm/utils/path_operations.py +100 -93
  374. claude_mpm/utils/robust_installer.py +172 -164
  375. claude_mpm/utils/session_logging.py +30 -23
  376. claude_mpm/utils/subprocess_utils.py +99 -61
  377. claude_mpm/validation/__init__.py +1 -1
  378. claude_mpm/validation/agent_validator.py +151 -111
  379. claude_mpm/validation/frontmatter_validator.py +92 -71
  380. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/METADATA +51 -2
  381. claude_mpm-4.0.3.dist-info/RECORD +402 -0
  382. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/entry_points.txt +1 -0
  383. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/licenses/LICENSE +1 -1
  384. claude_mpm/config/memory_guardian_config.py +0 -325
  385. claude_mpm/core/config_paths.py +0 -150
  386. claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
  387. claude_mpm/deployment_paths.py +0 -261
  388. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
  389. claude_mpm/models/state_models.py +0 -433
  390. claude_mpm/services/agent/__init__.py +0 -24
  391. claude_mpm/services/agent/deployment.py +0 -2548
  392. claude_mpm/services/agent/management.py +0 -598
  393. claude_mpm/services/agent/registry.py +0 -813
  394. claude_mpm/services/agents/registry/agent_registry.py +0 -813
  395. claude_mpm/services/communication/socketio.py +0 -1935
  396. claude_mpm/services/communication/websocket.py +0 -479
  397. claude_mpm/services/framework_claude_md_generator.py +0 -624
  398. claude_mpm/services/health_monitor.py +0 -893
  399. claude_mpm/services/infrastructure/memory_guardian.py +0 -770
  400. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
  401. claude_mpm/services/optimized_hook_service.py +0 -542
  402. claude_mpm/services/project_analyzer.py +0 -864
  403. claude_mpm/services/project_registry.py +0 -608
  404. claude_mpm/services/standalone_socketio_server.py +0 -1300
  405. claude_mpm/services/ticket_manager_di.py +0 -318
  406. claude_mpm/services/ticketing_service_original.py +0 -510
  407. claude_mpm/utils/paths.py +0 -395
  408. claude_mpm/utils/platform_memory.py +0 -524
  409. claude_mpm-3.9.9.dist-info/RECORD +0 -293
  410. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/WHEEL +0 -0
  411. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/top_level.txt +0 -0
@@ -1,1935 +0,0 @@
1
- """Socket.IO server for real-time monitoring of Claude MPM sessions.
2
-
3
- WHY: This provides a Socket.IO-based alternative to the WebSocket server,
4
- offering improved connection reliability and automatic reconnection.
5
- Socket.IO handles connection drops gracefully and provides better
6
- cross-platform compatibility.
7
- """
8
-
9
- import asyncio
10
- import json
11
- import logging
12
- import os
13
- import threading
14
- import time
15
- from datetime import datetime
16
- from typing import Set, Dict, Any, Optional, List
17
- from collections import deque
18
- from pathlib import Path
19
-
20
- try:
21
- import socketio
22
- import aiohttp
23
- from aiohttp import web
24
- SOCKETIO_AVAILABLE = True
25
- # Don't print at module level - this causes output during imports
26
- # Version will be logged when server is actually started
27
- except ImportError:
28
- SOCKETIO_AVAILABLE = False
29
- socketio = None
30
- aiohttp = None
31
- web = None
32
- # Don't print warnings at module level
33
-
34
- from claude_mpm.core.logging_config import get_logger, log_operation, log_performance_context
35
- from claude_mpm.deployment_paths import get_project_root, get_scripts_dir
36
- # from .socketio.handlers import EventHandlerRegistry, FileEventHandler, GitEventHandler # Module not found, commenting out
37
- from claude_mpm.core.constants import (
38
- SystemLimits,
39
- NetworkConfig,
40
- TimeoutConfig,
41
- PerformanceConfig
42
- )
43
- from claude_mpm.core.interfaces import SocketIOServiceInterface
44
- from claude_mpm.services.exceptions import SocketIOServerError as MPMConnectionError
45
-
46
-
47
- class SocketIOClientProxy:
48
- """Proxy that connects to an existing Socket.IO server as a client.
49
-
50
- WHY: In exec mode, a persistent Socket.IO server runs in a separate process.
51
- The hook handler in the Claude process needs a Socket.IO-like interface
52
- but shouldn't start another server. This proxy provides that interface
53
- while the actual events are handled by the persistent server.
54
- """
55
-
56
- def __init__(self, host: str = "localhost", port: int = 8765):
57
- self.host = host
58
- self.port = port
59
- self.logger = get_logger(__name__ + ".SocketIOClientProxy")
60
- self.running = True # Always "running" for compatibility
61
- self._sio_client = None
62
- self._client_thread = None
63
- self._client_loop = None
64
-
65
- def start_sync(self):
66
- """Start the Socket.IO client connection to the persistent server."""
67
- self.logger.debug(f"SocketIOClientProxy: Connecting to server on {self.host}:{self.port}")
68
- if SOCKETIO_AVAILABLE:
69
- self._start_client()
70
-
71
- def stop_sync(self):
72
- """Stop the Socket.IO client connection."""
73
- self.logger.debug(f"SocketIOClientProxy: Disconnecting from server")
74
- if self._sio_client:
75
- self._sio_client.disconnect()
76
-
77
- def _start_client(self):
78
- """Start Socket.IO client in a background thread."""
79
- def run_client():
80
- self._client_loop = asyncio.new_event_loop()
81
- asyncio.set_event_loop(self._client_loop)
82
- try:
83
- self._client_loop.run_until_complete(self._connect_and_run())
84
- except Exception as e:
85
- self.logger.error(f"SocketIOClientProxy client thread error: {e}")
86
- finally:
87
- self._client_loop.close()
88
-
89
- self._client_thread = threading.Thread(target=run_client, daemon=True)
90
- self._client_thread.start()
91
- # Give it a moment to connect
92
- time.sleep(0.2)
93
-
94
- async def _connect_and_run(self):
95
- """Connect to the persistent Socket.IO server and keep connection alive."""
96
- try:
97
- self._sio_client = socketio.AsyncClient()
98
-
99
- @self._sio_client.event
100
- async def connect():
101
- self.logger.info(f"SocketIOClientProxy: Connected to server at http://{self.host}:{self.port}")
102
-
103
- @self._sio_client.event
104
- async def disconnect():
105
- self.logger.info(f"SocketIOClientProxy: Disconnected from server")
106
-
107
- # Connect to the server
108
- await self._sio_client.connect(f'http://127.0.0.1:{self.port}')
109
-
110
- # Keep the connection alive until stopped
111
- while self.running:
112
- await asyncio.sleep(1)
113
-
114
- except Exception as e:
115
- self.logger.error(f"SocketIOClientProxy: Connection error: {e}")
116
- self._sio_client = None
117
-
118
- def broadcast_event(self, event_type: str, data: Dict[str, Any]):
119
- """Send event to the persistent Socket.IO server."""
120
- if not SOCKETIO_AVAILABLE:
121
- return
122
-
123
- # Ensure client is started
124
- if not self._client_thread or not self._client_thread.is_alive():
125
- self.logger.debug(f"SocketIOClientProxy: Starting client for {event_type}")
126
- self._start_client()
127
-
128
- if self._sio_client and self._sio_client.connected:
129
- try:
130
- event = {
131
- "type": event_type,
132
- "timestamp": datetime.now().isoformat(),
133
- "data": data
134
- }
135
-
136
- # Send event safely using run_coroutine_threadsafe
137
- if hasattr(self, '_client_loop') and self._client_loop and not self._client_loop.is_closed():
138
- try:
139
- future = asyncio.run_coroutine_threadsafe(
140
- self._sio_client.emit('claude_event', event),
141
- self._client_loop
142
- )
143
- # Don't wait for the result to avoid blocking
144
- self.logger.debug(f"SocketIOClientProxy: Scheduled emit for {event_type}")
145
- except Exception as e:
146
- self.logger.error(f"SocketIOClientProxy: Failed to schedule emit for {event_type}: {e}")
147
- else:
148
- self.logger.warning(f"SocketIOClientProxy: Client event loop not available for {event_type}")
149
-
150
- self.logger.debug(f"SocketIOClientProxy: Sent event {event_type}")
151
- except Exception as e:
152
- self.logger.error(f"SocketIOClientProxy: Failed to send event {event_type}: {e}")
153
- else:
154
- self.logger.warning(f"SocketIOClientProxy: Client not ready for {event_type}")
155
-
156
- # Compatibility methods for WebSocketServer interface
157
- def session_started(self, session_id: str, launch_method: str, working_dir: str):
158
- self.logger.debug(f"SocketIOClientProxy: Session started {session_id}")
159
-
160
- def session_ended(self):
161
- self.logger.debug(f"SocketIOClientProxy: Session ended")
162
-
163
- def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
164
- self.logger.debug(f"SocketIOClientProxy: Claude status {status}")
165
-
166
- def agent_delegated(self, agent: str, task: str, status: str = "started"):
167
- self.logger.debug(f"SocketIOClientProxy: Agent {agent} delegated")
168
-
169
- def todo_updated(self, todos: List[Dict[str, Any]]):
170
- self.logger.debug(f"SocketIOClientProxy: Todo updated ({len(todos)} todos)")
171
-
172
-
173
- class SocketIOServer(SocketIOServiceInterface):
174
- """Socket.IO server for broadcasting Claude MPM events.
175
-
176
- WHY: Socket.IO provides better connection reliability than raw WebSockets,
177
- with automatic reconnection, fallback transports, and better error handling.
178
- It maintains the same event interface as WebSocketServer for compatibility.
179
- """
180
-
181
- def __init__(self, host: str = "localhost", port: int = 8765):
182
- self.host = host
183
- self.port = port
184
- self.logger = get_logger(__name__)
185
- self.clients: Set[str] = set() # Store session IDs instead of connection objects
186
- self.event_history: deque = deque(maxlen=SystemLimits.MAX_EVENT_HISTORY)
187
- self.sio = None
188
- self.app = None
189
- self.runner = None
190
- self.site = None
191
- self.loop = None
192
- self.thread = None
193
- self.running = False
194
-
195
- # Session state
196
- self.session_id = None
197
- self.session_start = None
198
- self.claude_status = "stopped"
199
- self.claude_pid = None
200
-
201
- if not SOCKETIO_AVAILABLE:
202
- self.logger.warning("Socket.IO support not available. Install 'python-socketio' and 'aiohttp' packages to enable.")
203
- else:
204
- # Log version info when server is actually created
205
- try:
206
- version = getattr(socketio, '__version__', 'unknown')
207
- self.logger.info(f"Socket.IO server using python-socketio v{version}")
208
- except:
209
- self.logger.info("Socket.IO server using python-socketio (version unavailable)")
210
-
211
- def start_sync(self):
212
- """Start the Socket.IO server in a background thread (synchronous version)."""
213
- if not SOCKETIO_AVAILABLE:
214
- self.logger.debug("Socket.IO server skipped - required packages not installed")
215
- return
216
-
217
- if self.running:
218
- self.logger.debug(f"Socket.IO server already running on port {self.port}")
219
- return
220
-
221
- self.running = True
222
- self.thread = threading.Thread(target=self._run_server, daemon=True)
223
- self.thread.start()
224
- self.logger.info(f"🚀 Socket.IO server STARTING on http://{self.host}:{self.port}")
225
- self.logger.info(f"🔧 Thread created: {self.thread.name} (daemon={self.thread.daemon})")
226
-
227
- # Give server a moment to start
228
- time.sleep(0.1)
229
-
230
- if self.thread.is_alive():
231
- self.logger.info(f"✅ Socket.IO server thread is alive and running")
232
- else:
233
- self.logger.error(f"❌ Socket.IO server thread failed to start!")
234
-
235
- def stop_sync(self):
236
- """Stop the Socket.IO server (synchronous version)."""
237
- self.running = False
238
- if self.loop:
239
- asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop)
240
- if self.thread:
241
- self.thread.join(timeout=TimeoutConfig.THREAD_JOIN_TIMEOUT)
242
- self.logger.info("Socket.IO server stopped")
243
-
244
- def _run_server(self):
245
- """Run the server event loop."""
246
- self.logger.info(f"🔄 _run_server starting on thread: {threading.current_thread().name}")
247
- self.loop = asyncio.new_event_loop()
248
- asyncio.set_event_loop(self.loop)
249
- self.logger.info(f"📡 Event loop created and set for Socket.IO server")
250
-
251
- try:
252
- self.logger.info(f"🎯 About to start _serve() coroutine")
253
- self.loop.run_until_complete(self._serve())
254
- except Exception as e:
255
- self.logger.error(f"❌ Socket.IO server error in _run_server: {e}")
256
- import traceback
257
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
258
- finally:
259
- self.logger.info(f"🔚 Socket.IO server _run_server shutting down")
260
- self.loop.close()
261
-
262
- async def _serve(self):
263
- """Start the Socket.IO server."""
264
- try:
265
- self.logger.info(f"🔌 _serve() starting - attempting to bind to {self.host}:{self.port}")
266
-
267
- # Create Socket.IO server with improved configuration
268
- self.sio = socketio.AsyncServer(
269
- cors_allowed_origins="*",
270
- ping_timeout=NetworkConfig.PING_TIMEOUT,
271
- ping_interval=NetworkConfig.PING_INTERVAL,
272
- max_http_buffer_size=1000000,
273
- allow_upgrades=True,
274
- transports=['websocket', 'polling'],
275
- logger=False, # Reduce noise in logs
276
- engineio_logger=False
277
- )
278
-
279
- # Create aiohttp web application
280
- self.app = web.Application()
281
- self.sio.attach(self.app)
282
-
283
- # Add CORS middleware
284
- import aiohttp_cors
285
- cors = aiohttp_cors.setup(self.app, defaults={
286
- "*": aiohttp_cors.ResourceOptions(
287
- allow_credentials=True,
288
- expose_headers="*",
289
- allow_headers="*",
290
- allow_methods="*"
291
- )
292
- })
293
-
294
- # Add HTTP routes
295
- self.app.router.add_get('/health', self._handle_health)
296
- self.app.router.add_get('/status', self._handle_health)
297
- self.app.router.add_get('/api/git-diff', self._handle_git_diff)
298
- self.app.router.add_options('/api/git-diff', self._handle_cors_preflight)
299
- self.app.router.add_get('/api/file-content', self._handle_file_content)
300
- self.app.router.add_options('/api/file-content', self._handle_cors_preflight)
301
-
302
- # Add dashboard routes
303
- self.app.router.add_get('/', self._handle_dashboard)
304
- self.app.router.add_get('/dashboard', self._handle_dashboard)
305
-
306
- # Add static file serving for web assets
307
- static_path = self._find_static_path()
308
- if static_path and static_path.exists():
309
- self.app.router.add_static('/static/', path=str(static_path), name='static')
310
- self.logger.info(f"Static files served from: {static_path}")
311
- else:
312
- self.logger.warning("Static files directory not found - CSS/JS files will not be available")
313
-
314
- # Register event handlers
315
- self._register_events()
316
-
317
- # Start the server
318
- self.runner = web.AppRunner(self.app)
319
- await self.runner.setup()
320
-
321
- self.site = web.TCPSite(self.runner, self.host, self.port)
322
- try:
323
- await self.site.start()
324
- except OSError as e:
325
- if "Address already in use" in str(e) or "address already in use" in str(e).lower():
326
- raise MPMConnectionError(
327
- f"Port {self.port} is already in use",
328
- context={"host": self.host, "port": self.port, "error": str(e)}
329
- ) from e
330
- else:
331
- raise
332
-
333
- self.logger.info(f"🎉 Socket.IO server SUCCESSFULLY listening on http://{self.host}:{self.port}")
334
-
335
- # Keep server running
336
- loop_count = 0
337
- while self.running:
338
- await asyncio.sleep(0.1)
339
- loop_count += 1
340
- if loop_count % PerformanceConfig.LOG_EVERY_N_ITERATIONS == 0:
341
- self.logger.debug(f"🔄 Socket.IO server heartbeat - {len(self.clients)} clients connected")
342
-
343
- except Exception as e:
344
- self.logger.error(f"❌ Failed to start Socket.IO server: {e}")
345
- import traceback
346
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
347
- raise
348
-
349
- async def _shutdown(self):
350
- """Shutdown the server."""
351
- if self.site:
352
- await self.site.stop()
353
- if self.runner:
354
- await self.runner.cleanup()
355
-
356
- async def _handle_health(self, request):
357
- """Handle health check requests."""
358
- return web.json_response({
359
- "status": "healthy",
360
- "server": "claude-mpm-python-socketio",
361
- "timestamp": datetime.utcnow().isoformat() + "Z",
362
- "port": self.port,
363
- "host": self.host,
364
- "clients_connected": len(self.clients)
365
- }, headers={
366
- 'Access-Control-Allow-Origin': '*',
367
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
368
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
369
- })
370
-
371
- def _find_static_path(self):
372
- """Find the static files directory using multiple approaches.
373
-
374
- WHY: Static files need to be found in both development and installed environments.
375
- This uses the same multi-approach pattern as dashboard HTML resolution.
376
- """
377
-
378
- # Approach 1: Use module-relative path (works in installed environment)
379
- try:
380
- import claude_mpm.dashboard
381
-
382
- # Try __file__ attribute first
383
- if hasattr(claude_mpm.dashboard, '__file__') and claude_mpm.dashboard.__file__:
384
- dashboard_module_path = Path(claude_mpm.dashboard.__file__).parent
385
- candidate_path = dashboard_module_path / "static"
386
- if candidate_path.exists():
387
- self.logger.info(f"Found static files using module __file__ path: {candidate_path}")
388
- return candidate_path
389
-
390
- # Try __path__ attribute for namespace packages
391
- elif hasattr(claude_mpm.dashboard, '__path__') and claude_mpm.dashboard.__path__:
392
- # __path__ is a list, take the first entry
393
- dashboard_module_path = Path(claude_mpm.dashboard.__path__[0])
394
- candidate_path = dashboard_module_path / "static"
395
- if candidate_path.exists():
396
- self.logger.info(f"Found static files using module __path__: {candidate_path}")
397
- return candidate_path
398
-
399
- except Exception as e:
400
- self.logger.debug(f"Module-relative static path failed: {e}")
401
-
402
- # Approach 2: Use project root (works in development environment)
403
- try:
404
- candidate_path = get_project_root() / 'src' / 'claude_mpm' / 'dashboard' / 'static'
405
- if candidate_path.exists():
406
- self.logger.info(f"Found static files using project root: {candidate_path}")
407
- return candidate_path
408
- except Exception as e:
409
- self.logger.debug(f"Project root static path failed: {e}")
410
-
411
- # Approach 3: Search for static files in package installation
412
- try:
413
- candidate_path = get_project_root() / 'claude_mpm' / 'dashboard' / 'static'
414
- if candidate_path.exists():
415
- self.logger.info(f"Found static files using package path: {candidate_path}")
416
- return candidate_path
417
- except Exception as e:
418
- self.logger.debug(f"Package static path failed: {e}")
419
-
420
- return None
421
-
422
- async def _handle_dashboard(self, request):
423
- """Serve the dashboard HTML file."""
424
- # Try to find dashboard path using multiple approaches
425
- dashboard_path = None
426
-
427
- # Approach 1: Use module-relative path (works in installed environment)
428
- try:
429
- import claude_mpm.dashboard
430
-
431
- # Try __file__ attribute first
432
- if hasattr(claude_mpm.dashboard, '__file__') and claude_mpm.dashboard.__file__:
433
- dashboard_module_path = Path(claude_mpm.dashboard.__file__).parent
434
- candidate_path = dashboard_module_path / "templates" / "index.html"
435
- if candidate_path.exists():
436
- dashboard_path = candidate_path
437
- self.logger.info(f"Found dashboard using module __file__ path: {dashboard_path}")
438
-
439
- # Try __path__ attribute for namespace packages
440
- elif hasattr(claude_mpm.dashboard, '__path__') and claude_mpm.dashboard.__path__:
441
- # __path__ is a list, take the first entry
442
- dashboard_module_path = Path(claude_mpm.dashboard.__path__[0])
443
- candidate_path = dashboard_module_path / "templates" / "index.html"
444
- if candidate_path.exists():
445
- dashboard_path = candidate_path
446
- self.logger.info(f"Found dashboard using module __path__: {dashboard_path}")
447
-
448
- except Exception as e:
449
- self.logger.debug(f"Module-relative path failed: {e}")
450
-
451
- # Approach 2: Use project root (works in development environment)
452
- if dashboard_path is None:
453
- try:
454
- candidate_path = get_project_root() / 'src' / 'claude_mpm' / 'dashboard' / 'templates' / 'index.html'
455
- if candidate_path.exists():
456
- dashboard_path = candidate_path
457
- self.logger.info(f"Found dashboard using project root: {dashboard_path}")
458
- except Exception as e:
459
- self.logger.debug(f"Project root path failed: {e}")
460
-
461
- # Approach 3: Search for dashboard in package installation
462
- if dashboard_path is None:
463
- try:
464
- candidate_path = get_project_root() / 'claude_mpm' / 'dashboard' / 'templates' / 'index.html'
465
- if candidate_path.exists():
466
- dashboard_path = candidate_path
467
- self.logger.info(f"Found dashboard using package path: {dashboard_path}")
468
- except Exception as e:
469
- self.logger.debug(f"Package path failed: {e}")
470
-
471
- if dashboard_path and dashboard_path.exists():
472
- return web.FileResponse(str(dashboard_path))
473
- else:
474
- error_msg = f"Dashboard not found. Searched paths:\n"
475
- error_msg += f"1. Module-relative: {dashboard_module_path / 'templates' / 'index.html' if 'dashboard_module_path' in locals() else 'N/A'}\n"
476
- error_msg += f"2. Development: {get_project_root() / 'src' / 'claude_mpm' / 'dashboard' / 'templates' / 'index.html'}\n"
477
- error_msg += f"3. Package: {get_project_root() / 'claude_mpm' / 'dashboard' / 'templates' / 'index.html'}"
478
- self.logger.error(error_msg)
479
- return web.Response(text=error_msg, status=404)
480
-
481
- async def _handle_cors_preflight(self, request):
482
- """Handle CORS preflight requests."""
483
- return web.Response(
484
- status=NetworkConfig.HTTP_OK,
485
- headers={
486
- 'Access-Control-Allow-Origin': '*',
487
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
488
- 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization',
489
- 'Access-Control-Max-Age': '86400'
490
- }
491
- )
492
-
493
- async def _handle_git_diff(self, request):
494
- """Handle git diff requests for file operations.
495
-
496
- Expected query parameters:
497
- - file: The file path to generate diff for
498
- - timestamp: ISO timestamp of the operation (optional)
499
- - working_dir: Working directory for git operations (optional)
500
- """
501
- try:
502
- # Extract query parameters
503
- file_path = request.query.get('file')
504
- timestamp = request.query.get('timestamp')
505
- working_dir = request.query.get('working_dir', os.getcwd())
506
-
507
- self.logger.info(f"Git diff API request: file={file_path}, timestamp={timestamp}, working_dir={working_dir}")
508
- self.logger.info(f"Git diff request details: query_params={dict(request.query)}, file_exists={os.path.exists(file_path) if file_path else False}")
509
-
510
- if not file_path:
511
- self.logger.warning("Git diff request missing file parameter")
512
- return web.json_response({
513
- "success": False,
514
- "error": "Missing required parameter: file"
515
- }, status=400, headers={
516
- 'Access-Control-Allow-Origin': '*',
517
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
518
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
519
- })
520
-
521
- self.logger.debug(f"Git diff requested for file: {file_path}, timestamp: {timestamp}")
522
-
523
- # Generate git diff using the git handler's method
524
- if hasattr(self, 'git_handler') and self.git_handler:
525
- diff_result = await self.git_handler.generate_git_diff(file_path, timestamp, working_dir)
526
- else:
527
- # Fallback to old method if handler not available
528
- diff_result = await self._generate_git_diff(file_path, timestamp, working_dir)
529
-
530
- self.logger.info(f"Git diff result: success={diff_result.get('success', False)}, method={diff_result.get('method', 'unknown')}")
531
-
532
- return web.json_response(diff_result, headers={
533
- 'Access-Control-Allow-Origin': '*',
534
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
535
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
536
- })
537
-
538
- except Exception as e:
539
- self.logger.error(f"Error generating git diff: {e}")
540
- import traceback
541
- self.logger.error(f"Git diff error traceback: {traceback.format_exc()}")
542
- return web.json_response({
543
- "success": False,
544
- "error": f"Failed to generate git diff: {str(e)}"
545
- }, status=NetworkConfig.HTTP_INTERNAL_ERROR, headers={
546
- 'Access-Control-Allow-Origin': '*',
547
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
548
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
549
- })
550
-
551
- async def _handle_file_content(self, request):
552
- """Handle file content requests via HTTP API.
553
-
554
- Expected query parameters:
555
- - file_path: The file path to read
556
- - working_dir: Working directory for file operations (optional)
557
- - max_size: Maximum file size in bytes (optional, default 1MB)
558
- """
559
- try:
560
- # Extract query parameters
561
- file_path = request.query.get('file_path')
562
- working_dir = request.query.get('working_dir', os.getcwd())
563
- max_size = int(request.query.get('max_size', SystemLimits.MAX_FILE_SIZE))
564
-
565
- self.logger.info(f"File content API request: file_path={file_path}, working_dir={working_dir}")
566
-
567
- if not file_path:
568
- self.logger.warning("File content request missing file_path parameter")
569
- return web.json_response({
570
- "success": False,
571
- "error": "Missing required parameter: file_path"
572
- }, status=400, headers={
573
- 'Access-Control-Allow-Origin': '*',
574
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
575
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
576
- })
577
-
578
- # Use the file handler's safe reading logic
579
- if hasattr(self, 'file_handler') and self.file_handler:
580
- result = await self.file_handler._read_file_safely(file_path, working_dir, max_size)
581
- else:
582
- # Fallback to old method if handler not available
583
- result = await self._read_file_safely(file_path, working_dir, max_size)
584
-
585
- status_code = 200 if result.get('success') else 400
586
- return web.json_response(result, status=status_code, headers={
587
- 'Access-Control-Allow-Origin': '*',
588
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
589
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
590
- })
591
-
592
- except Exception as e:
593
- self.logger.error(f"Error reading file content: {e}")
594
- import traceback
595
- self.logger.error(f"File content error traceback: {traceback.format_exc()}")
596
- return web.json_response({
597
- "success": False,
598
- "error": f"Failed to read file: {str(e)}"
599
- }, status=NetworkConfig.HTTP_INTERNAL_ERROR, headers={
600
- 'Access-Control-Allow-Origin': '*',
601
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
602
- 'Access-Control-Allow-Headers': 'Content-Type, Accept'
603
- })
604
-
605
- async def _read_file_safely(self, file_path: str, working_dir: str = None, max_size: int = SystemLimits.MAX_FILE_SIZE):
606
- """Safely read file content with security checks.
607
-
608
- This method contains the core file reading logic that can be used by both
609
- HTTP API endpoints and Socket.IO event handlers.
610
-
611
- Args:
612
- file_path: Path to the file to read
613
- working_dir: Working directory (defaults to current directory)
614
- max_size: Maximum file size in bytes
615
-
616
- Returns:
617
- dict: Response with success status, content, and metadata
618
- """
619
- try:
620
- if working_dir is None:
621
- working_dir = os.getcwd()
622
-
623
- # Resolve absolute path based on working directory
624
- if not os.path.isabs(file_path):
625
- full_path = os.path.join(working_dir, file_path)
626
- else:
627
- full_path = file_path
628
-
629
- # Security check: ensure file is within working directory or project
630
- try:
631
- real_path = os.path.realpath(full_path)
632
- real_working_dir = os.path.realpath(working_dir)
633
-
634
- # Allow access to files within working directory or the project root
635
- project_root = os.path.realpath(get_project_root())
636
- allowed_paths = [real_working_dir, project_root]
637
-
638
- is_allowed = any(real_path.startswith(allowed_path) for allowed_path in allowed_paths)
639
-
640
- if not is_allowed:
641
- return {
642
- 'success': False,
643
- 'error': 'Access denied: file is outside allowed directories',
644
- 'file_path': file_path
645
- }
646
-
647
- except Exception as path_error:
648
- self.logger.error(f"Path validation error: {path_error}")
649
- return {
650
- 'success': False,
651
- 'error': 'Invalid file path',
652
- 'file_path': file_path
653
- }
654
-
655
- # Check if file exists
656
- if not os.path.exists(real_path):
657
- return {
658
- 'success': False,
659
- 'error': 'File does not exist',
660
- 'file_path': file_path
661
- }
662
-
663
- # Check if it's a file (not directory)
664
- if not os.path.isfile(real_path):
665
- return {
666
- 'success': False,
667
- 'error': 'Path is not a file',
668
- 'file_path': file_path
669
- }
670
-
671
- # Check file size
672
- file_size = os.path.getsize(real_path)
673
- if file_size > max_size:
674
- return {
675
- 'success': False,
676
- 'error': f'File too large ({file_size} bytes). Maximum allowed: {max_size} bytes',
677
- 'file_path': file_path,
678
- 'file_size': file_size
679
- }
680
-
681
- # Read file content
682
- try:
683
- with open(real_path, 'r', encoding='utf-8') as f:
684
- content = f.read()
685
-
686
- # Get file extension for syntax highlighting hint
687
- _, ext = os.path.splitext(real_path)
688
-
689
- return {
690
- 'success': True,
691
- 'file_path': file_path,
692
- 'content': content,
693
- 'file_size': file_size,
694
- 'extension': ext.lower(),
695
- 'encoding': 'utf-8'
696
- }
697
-
698
- except UnicodeDecodeError:
699
- # Try reading as binary if UTF-8 fails
700
- try:
701
- with open(real_path, 'rb') as f:
702
- binary_content = f.read()
703
-
704
- # Check if it's a text file by looking for common text patterns
705
- try:
706
- text_content = binary_content.decode('latin-1')
707
- if '\x00' in text_content:
708
- # Binary file
709
- return {
710
- 'success': False,
711
- 'error': 'File appears to be binary and cannot be displayed as text',
712
- 'file_path': file_path,
713
- 'file_size': file_size
714
- }
715
- else:
716
- # Text file with different encoding
717
- _, ext = os.path.splitext(real_path)
718
- return {
719
- 'success': True,
720
- 'file_path': file_path,
721
- 'content': text_content,
722
- 'file_size': file_size,
723
- 'extension': ext.lower(),
724
- 'encoding': 'latin-1'
725
- }
726
- except Exception:
727
- return {
728
- 'success': False,
729
- 'error': 'File encoding not supported',
730
- 'file_path': file_path
731
- }
732
- except Exception as read_error:
733
- return {
734
- 'success': False,
735
- 'error': f'Failed to read file: {str(read_error)}',
736
- 'file_path': file_path
737
- }
738
-
739
- except Exception as e:
740
- self.logger.error(f"Error in _read_file_safely: {e}")
741
- return {
742
- 'success': False,
743
- 'error': str(e),
744
- 'file_path': file_path
745
- }
746
-
747
- async def _generate_git_diff(self, file_path: str, timestamp: Optional[str] = None, working_dir: str = None):
748
- """Generate git diff for a specific file operation.
749
-
750
- WHY: This method generates a git diff showing the changes made to a file
751
- during a specific write operation. It uses git log and show commands to
752
- find the most relevant commit around the specified timestamp.
753
-
754
- Args:
755
- file_path: Path to the file relative to the git repository
756
- timestamp: ISO timestamp of the file operation (optional)
757
- working_dir: Working directory containing the git repository
758
-
759
- Returns:
760
- dict: Contains diff content, metadata, and status information
761
- """
762
- try:
763
- # If file_path is absolute, determine its git repository
764
- if os.path.isabs(file_path):
765
- # Find the directory containing the file
766
- file_dir = os.path.dirname(file_path)
767
- if os.path.exists(file_dir):
768
- # Try to find the git root from the file's directory
769
- current_dir = file_dir
770
- while current_dir != "/" and current_dir:
771
- if os.path.exists(os.path.join(current_dir, ".git")):
772
- working_dir = current_dir
773
- self.logger.info(f"Found git repository at: {working_dir}")
774
- break
775
- current_dir = os.path.dirname(current_dir)
776
- else:
777
- # If no git repo found, use the file's directory
778
- working_dir = file_dir
779
- self.logger.info(f"No git repo found, using file's directory: {working_dir}")
780
-
781
- # Handle case where working_dir is None, empty string, or 'Unknown'
782
- original_working_dir = working_dir
783
- if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '':
784
- working_dir = os.getcwd()
785
- self.logger.info(f"[GIT-DIFF-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
786
- else:
787
- self.logger.info(f"[GIT-DIFF-DEBUG] Using provided working_dir: {working_dir}")
788
-
789
- # For read-only git operations, we can work from any directory
790
- # by passing the -C flag to git commands instead of changing directories
791
- original_cwd = os.getcwd()
792
- try:
793
- # We'll use git -C <working_dir> for all commands instead of chdir
794
-
795
- # Check if this is a git repository
796
- git_check = await asyncio.create_subprocess_exec(
797
- 'git', '-C', working_dir, 'rev-parse', '--git-dir',
798
- stdout=asyncio.subprocess.PIPE,
799
- stderr=asyncio.subprocess.PIPE
800
- )
801
- await git_check.communicate()
802
-
803
- if git_check.returncode != 0:
804
- return {
805
- "success": False,
806
- "error": "Not a git repository",
807
- "file_path": file_path,
808
- "working_dir": working_dir
809
- }
810
-
811
- # Get the absolute path of the file relative to git root
812
- git_root_proc = await asyncio.create_subprocess_exec(
813
- 'git', '-C', working_dir, 'rev-parse', '--show-toplevel',
814
- stdout=asyncio.subprocess.PIPE,
815
- stderr=asyncio.subprocess.PIPE
816
- )
817
- git_root_output, _ = await git_root_proc.communicate()
818
-
819
- if git_root_proc.returncode != 0:
820
- return {"success": False, "error": "Failed to determine git root directory"}
821
-
822
- git_root = git_root_output.decode().strip()
823
-
824
- # Make file_path relative to git root if it's absolute
825
- if os.path.isabs(file_path):
826
- try:
827
- file_path = os.path.relpath(file_path, git_root)
828
- except ValueError:
829
- # File is not under git root
830
- pass
831
-
832
- # If timestamp is provided, try to find commits around that time
833
- if timestamp:
834
- # Convert timestamp to git format
835
- try:
836
- from datetime import datetime
837
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
838
- git_since = dt.strftime('%Y-%m-%d %H:%M:%S')
839
-
840
- # Find commits that modified this file around the timestamp
841
- log_proc = await asyncio.create_subprocess_exec(
842
- 'git', '-C', working_dir, 'log', '--oneline', '--since', git_since,
843
- '--until', f'{git_since} +1 hour', '--', file_path,
844
- stdout=asyncio.subprocess.PIPE,
845
- stderr=asyncio.subprocess.PIPE
846
- )
847
- log_output, _ = await log_proc.communicate()
848
-
849
- if log_proc.returncode == 0 and log_output:
850
- # Get the most recent commit hash
851
- commits = log_output.decode().strip().split('\n')
852
- if commits and commits[0]:
853
- commit_hash = commits[0].split()[0]
854
-
855
- # Get the diff for this specific commit
856
- diff_proc = await asyncio.create_subprocess_exec(
857
- 'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
858
- stdout=asyncio.subprocess.PIPE,
859
- stderr=asyncio.subprocess.PIPE
860
- )
861
- diff_output, diff_error = await diff_proc.communicate()
862
-
863
- if diff_proc.returncode == 0:
864
- return {
865
- "success": True,
866
- "diff": diff_output.decode(),
867
- "commit_hash": commit_hash,
868
- "file_path": file_path,
869
- "method": "timestamp_based",
870
- "timestamp": timestamp
871
- }
872
- except Exception as e:
873
- self.logger.warning(f"Failed to parse timestamp or find commits: {e}")
874
-
875
- # Fallback: Get the most recent change to the file
876
- log_proc = await asyncio.create_subprocess_exec(
877
- 'git', '-C', working_dir, 'log', '-1', '--oneline', '--', file_path,
878
- stdout=asyncio.subprocess.PIPE,
879
- stderr=asyncio.subprocess.PIPE
880
- )
881
- log_output, _ = await log_proc.communicate()
882
-
883
- if log_proc.returncode == 0 and log_output:
884
- commit_hash = log_output.decode().strip().split()[0]
885
-
886
- # Get the diff for the most recent commit
887
- diff_proc = await asyncio.create_subprocess_exec(
888
- 'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
889
- stdout=asyncio.subprocess.PIPE,
890
- stderr=asyncio.subprocess.PIPE
891
- )
892
- diff_output, diff_error = await diff_proc.communicate()
893
-
894
- if diff_proc.returncode == 0:
895
- return {
896
- "success": True,
897
- "diff": diff_output.decode(),
898
- "commit_hash": commit_hash,
899
- "file_path": file_path,
900
- "method": "latest_commit",
901
- "timestamp": timestamp
902
- }
903
-
904
- # Try to show unstaged changes first
905
- diff_proc = await asyncio.create_subprocess_exec(
906
- 'git', '-C', working_dir, 'diff', '--', file_path,
907
- stdout=asyncio.subprocess.PIPE,
908
- stderr=asyncio.subprocess.PIPE
909
- )
910
- diff_output, _ = await diff_proc.communicate()
911
-
912
- if diff_proc.returncode == 0 and diff_output.decode().strip():
913
- return {
914
- "success": True,
915
- "diff": diff_output.decode(),
916
- "commit_hash": "unstaged_changes",
917
- "file_path": file_path,
918
- "method": "unstaged_changes",
919
- "timestamp": timestamp
920
- }
921
-
922
- # Then try staged changes
923
- diff_proc = await asyncio.create_subprocess_exec(
924
- 'git', '-C', working_dir, 'diff', '--cached', '--', file_path,
925
- stdout=asyncio.subprocess.PIPE,
926
- stderr=asyncio.subprocess.PIPE
927
- )
928
- diff_output, _ = await diff_proc.communicate()
929
-
930
- if diff_proc.returncode == 0 and diff_output.decode().strip():
931
- return {
932
- "success": True,
933
- "diff": diff_output.decode(),
934
- "commit_hash": "staged_changes",
935
- "file_path": file_path,
936
- "method": "staged_changes",
937
- "timestamp": timestamp
938
- }
939
-
940
- # Final fallback: Show changes against HEAD
941
- diff_proc = await asyncio.create_subprocess_exec(
942
- 'git', '-C', working_dir, 'diff', 'HEAD', '--', file_path,
943
- stdout=asyncio.subprocess.PIPE,
944
- stderr=asyncio.subprocess.PIPE
945
- )
946
- diff_output, _ = await diff_proc.communicate()
947
-
948
- if diff_proc.returncode == 0:
949
- working_diff = diff_output.decode()
950
- if working_diff.strip():
951
- return {
952
- "success": True,
953
- "diff": working_diff,
954
- "commit_hash": "working_directory",
955
- "file_path": file_path,
956
- "method": "working_directory",
957
- "timestamp": timestamp
958
- }
959
-
960
- # Check if file is tracked by git
961
- status_proc = await asyncio.create_subprocess_exec(
962
- 'git', '-C', working_dir, 'ls-files', '--', file_path,
963
- stdout=asyncio.subprocess.PIPE,
964
- stderr=asyncio.subprocess.PIPE
965
- )
966
- status_output, _ = await status_proc.communicate()
967
-
968
- is_tracked = status_proc.returncode == 0 and status_output.decode().strip()
969
-
970
- if not is_tracked:
971
- # File is not tracked by git
972
- return {
973
- "success": False,
974
- "error": "This file is not tracked by git",
975
- "file_path": file_path,
976
- "working_dir": working_dir,
977
- "suggestions": [
978
- "This file has not been added to git yet",
979
- "Use 'git add' to track this file before viewing its diff",
980
- "Git diff can only show changes for files that are tracked by git"
981
- ]
982
- }
983
-
984
- # File is tracked but has no changes to show
985
- suggestions = [
986
- "The file may not have any committed changes yet",
987
- "The file may have been added but not committed",
988
- "The timestamp may be outside the git history range"
989
- ]
990
-
991
- if os.path.isabs(file_path) and not file_path.startswith(os.getcwd()):
992
- current_repo = os.path.basename(os.getcwd())
993
- file_repo = "unknown"
994
- # Try to extract repository name from path
995
- path_parts = file_path.split("/")
996
- if "Projects" in path_parts:
997
- idx = path_parts.index("Projects")
998
- if idx + 1 < len(path_parts):
999
- file_repo = path_parts[idx + 1]
1000
-
1001
- suggestions.clear()
1002
- suggestions.append(f"This file is from the '{file_repo}' repository")
1003
- suggestions.append(f"The git diff viewer is running from the '{current_repo}' repository")
1004
- suggestions.append("Git diff can only show changes for files in the current repository")
1005
- suggestions.append("To view changes for this file, run the monitoring dashboard from its repository")
1006
-
1007
- return {
1008
- "success": False,
1009
- "error": "No git history found for this file",
1010
- "file_path": file_path,
1011
- "suggestions": suggestions
1012
- }
1013
-
1014
- finally:
1015
- os.chdir(original_cwd)
1016
-
1017
- except Exception as e:
1018
- self.logger.error(f"Error in _generate_git_diff: {e}")
1019
- return {
1020
- "success": False,
1021
- "error": f"Git diff generation failed: {str(e)}",
1022
- "file_path": file_path
1023
- }
1024
-
1025
-
1026
- def _register_events(self):
1027
- """Register Socket.IO event handlers.
1028
-
1029
- WHY: This method now uses the EventHandlerRegistry to manage all event
1030
- handlers in a modular way. Each handler focuses on a specific domain,
1031
- reducing complexity and improving maintainability.
1032
- """
1033
- # Handler registry not available - skip handler-based registration
1034
- # # Initialize the event handler registry
1035
- # self.event_registry = EventHandlerRegistry(self)
1036
- # self.event_registry.initialize()
1037
-
1038
- # # Register all events from all handlers
1039
- # self.event_registry.register_all_events()
1040
-
1041
- # # Keep handler instances for HTTP endpoint compatibility
1042
- # self.file_handler = self.event_registry.get_handler(FileEventHandler)
1043
- # self.git_handler = self.event_registry.get_handler(GitEventHandler)
1044
-
1045
- # self.logger.info("All Socket.IO events registered via handler system")
1046
-
1047
- # Note: The actual event registration is now handled by individual
1048
- # handler classes in socketio/handlers/. This dramatically reduces
1049
- # the complexity of this method from 514 lines to under 20 lines.
1050
-
1051
- # Continue with old implementation since handlers are not available
1052
- # return # Early return to skip old implementation
1053
-
1054
- @self.sio.event
1055
- async def connect(sid, environ, *args):
1056
- """Handle client connection."""
1057
- self.clients.add(sid)
1058
- client_addr = environ.get('REMOTE_ADDR', 'unknown')
1059
- user_agent = environ.get('HTTP_USER_AGENT', 'unknown')
1060
- self.logger.info(f"🔗 NEW CLIENT CONNECTED: {sid} from {client_addr}")
1061
- self.logger.info(f"📱 User Agent: {user_agent[:100]}...")
1062
- self.logger.info(f"📈 Total clients now: {len(self.clients)}")
1063
-
1064
- # Send initial status immediately with enhanced data
1065
- status_data = {
1066
- "server": "claude-mpm-python-socketio",
1067
- "timestamp": datetime.utcnow().isoformat() + "Z",
1068
- "clients_connected": len(self.clients),
1069
- "session_id": self.session_id,
1070
- "claude_status": self.claude_status,
1071
- "claude_pid": self.claude_pid,
1072
- "server_version": "2.0.0",
1073
- "client_id": sid
1074
- }
1075
-
1076
- try:
1077
- await self.sio.emit('status', status_data, room=sid)
1078
- await self.sio.emit('welcome', {
1079
- "message": "Connected to Claude MPM Socket.IO server",
1080
- "client_id": sid,
1081
- "server_time": datetime.utcnow().isoformat() + "Z"
1082
- }, room=sid)
1083
-
1084
- # Automatically send the last 50 events to new clients
1085
- await self._send_event_history(sid, limit=50)
1086
-
1087
- self.logger.debug(f"✅ Sent welcome messages and event history to client {sid}")
1088
- except Exception as e:
1089
- self.logger.error(f"❌ Failed to send welcome to client {sid}: {e}")
1090
- import traceback
1091
- self.logger.error(f"Full traceback: {traceback.format_exc()}")
1092
-
1093
- @self.sio.event
1094
- async def disconnect(sid):
1095
- """Handle client disconnection."""
1096
- if sid in self.clients:
1097
- self.clients.remove(sid)
1098
- self.logger.info(f"🔌 CLIENT DISCONNECTED: {sid}")
1099
- self.logger.info(f"📉 Total clients now: {len(self.clients)}")
1100
- else:
1101
- self.logger.warning(f"⚠️ Attempted to disconnect unknown client: {sid}")
1102
-
1103
- @self.sio.event
1104
- async def get_status(sid):
1105
- """Handle status request."""
1106
- # Send compatible status event (not claude_event)
1107
- status_data = {
1108
- "server": "claude-mpm-python-socketio",
1109
- "timestamp": datetime.utcnow().isoformat() + "Z",
1110
- "clients_connected": len(self.clients),
1111
- "session_id": self.session_id,
1112
- "claude_status": self.claude_status,
1113
- "claude_pid": self.claude_pid
1114
- }
1115
- await self.sio.emit('status', status_data, room=sid)
1116
- self.logger.debug(f"Sent status response to client {sid}")
1117
-
1118
- @self.sio.event
1119
- async def get_history(sid, data=None):
1120
- """Handle history request."""
1121
- params = data or {}
1122
- event_types = params.get("event_types", [])
1123
- limit = min(params.get("limit", 100), len(self.event_history))
1124
-
1125
- await self._send_event_history(sid, event_types=event_types, limit=limit)
1126
-
1127
- @self.sio.event
1128
- async def request_history(sid, data=None):
1129
- """Handle legacy history request (for client compatibility)."""
1130
- # This handles the 'request.history' event that the client currently emits
1131
- params = data or {}
1132
- event_types = params.get("event_types", [])
1133
- limit = min(params.get("limit", 50), len(self.event_history))
1134
-
1135
- await self._send_event_history(sid, event_types=event_types, limit=limit)
1136
-
1137
- @self.sio.event
1138
- async def subscribe(sid, data=None):
1139
- """Handle subscription request."""
1140
- channels = data.get("channels", ["*"]) if data else ["*"]
1141
- await self.sio.emit('subscribed', {
1142
- "channels": channels
1143
- }, room=sid)
1144
-
1145
- @self.sio.event
1146
- async def claude_event(sid, data):
1147
- """Handle events from client proxies."""
1148
- # Store in history
1149
- self.event_history.append(data)
1150
- self.logger.debug(f"📚 Event from client stored in history (total: {len(self.event_history)})")
1151
-
1152
- # Re-broadcast to all other clients
1153
- await self.sio.emit('claude_event', data, skip_sid=sid)
1154
-
1155
- @self.sio.event
1156
- async def get_git_branch(sid, working_dir=None):
1157
- """Get the current git branch for a directory"""
1158
- import subprocess
1159
- try:
1160
- self.logger.info(f"[GIT-BRANCH-DEBUG] get_git_branch called with working_dir: {repr(working_dir)} (type: {type(working_dir)})")
1161
-
1162
- # Handle case where working_dir is None, empty string, or common invalid states
1163
- original_working_dir = working_dir
1164
- invalid_states = [
1165
- None, '', 'Unknown', 'Loading...', 'Loading', 'undefined', 'null',
1166
- 'Not Connected', 'Invalid Directory', 'No Directory'
1167
- ]
1168
-
1169
- if working_dir in invalid_states or (isinstance(working_dir, str) and working_dir.strip() == ''):
1170
- working_dir = os.getcwd()
1171
- self.logger.info(f"[GIT-BRANCH-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
1172
- else:
1173
- self.logger.info(f"[GIT-BRANCH-DEBUG] Using provided working_dir: {working_dir}")
1174
-
1175
- # Additional validation for obviously invalid paths
1176
- if isinstance(working_dir, str):
1177
- working_dir = working_dir.strip()
1178
- # Check for null bytes or other invalid characters
1179
- if '\x00' in working_dir:
1180
- self.logger.warning(f"[GIT-BRANCH-DEBUG] working_dir contains null bytes, using cwd instead")
1181
- working_dir = os.getcwd()
1182
-
1183
- # Validate that the directory exists and is a valid path
1184
- if not os.path.exists(working_dir):
1185
- self.logger.info(f"[GIT-BRANCH-DEBUG] Directory does not exist: {working_dir} - responding gracefully")
1186
- await self.sio.emit('git_branch_response', {
1187
- 'success': False,
1188
- 'error': f'Directory not found',
1189
- 'working_dir': working_dir,
1190
- 'original_working_dir': original_working_dir,
1191
- 'detail': f'Path does not exist: {working_dir}'
1192
- }, room=sid)
1193
- return
1194
-
1195
- if not os.path.isdir(working_dir):
1196
- self.logger.info(f"[GIT-BRANCH-DEBUG] Path is not a directory: {working_dir} - responding gracefully")
1197
- await self.sio.emit('git_branch_response', {
1198
- 'success': False,
1199
- 'error': f'Not a directory',
1200
- 'working_dir': working_dir,
1201
- 'original_working_dir': original_working_dir,
1202
- 'detail': f'Path is not a directory: {working_dir}'
1203
- }, room=sid)
1204
- return
1205
-
1206
- self.logger.info(f"[GIT-BRANCH-DEBUG] Running git command in directory: {working_dir}")
1207
-
1208
- # Run git command to get current branch
1209
- result = subprocess.run(
1210
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
1211
- cwd=working_dir,
1212
- capture_output=True,
1213
- text=True
1214
- )
1215
-
1216
- self.logger.info(f"[GIT-BRANCH-DEBUG] Git command result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
1217
-
1218
- if result.returncode == 0:
1219
- branch = result.stdout.strip()
1220
- self.logger.info(f"[GIT-BRANCH-DEBUG] Successfully got git branch: {branch}")
1221
- await self.sio.emit('git_branch_response', {
1222
- 'success': True,
1223
- 'branch': branch,
1224
- 'working_dir': working_dir,
1225
- 'original_working_dir': original_working_dir
1226
- }, room=sid)
1227
- else:
1228
- self.logger.warning(f"[GIT-BRANCH-DEBUG] Git command failed: {result.stderr}")
1229
- await self.sio.emit('git_branch_response', {
1230
- 'success': False,
1231
- 'error': 'Not a git repository',
1232
- 'working_dir': working_dir,
1233
- 'original_working_dir': original_working_dir,
1234
- 'git_error': result.stderr
1235
- }, room=sid)
1236
-
1237
- except Exception as e:
1238
- self.logger.error(f"[GIT-BRANCH-DEBUG] Exception in get_git_branch: {e}")
1239
- import traceback
1240
- self.logger.error(f"[GIT-BRANCH-DEBUG] Stack trace: {traceback.format_exc()}")
1241
- await self.sio.emit('git_branch_response', {
1242
- 'success': False,
1243
- 'error': str(e),
1244
- 'working_dir': working_dir,
1245
- 'original_working_dir': original_working_dir
1246
- }, room=sid)
1247
-
1248
- @self.sio.event
1249
- async def check_file_tracked(sid, data):
1250
- """Check if a file is tracked by git"""
1251
- import subprocess
1252
- try:
1253
- file_path = data.get('file_path')
1254
- working_dir = data.get('working_dir', os.getcwd())
1255
-
1256
- if not file_path:
1257
- await self.sio.emit('file_tracked_response', {
1258
- 'success': False,
1259
- 'error': 'file_path is required',
1260
- 'file_path': file_path
1261
- }, room=sid)
1262
- return
1263
-
1264
- # Use git ls-files to check if file is tracked
1265
- result = subprocess.run(
1266
- ["git", "-C", working_dir, "ls-files", "--", file_path],
1267
- capture_output=True,
1268
- text=True
1269
- )
1270
-
1271
- is_tracked = result.returncode == 0 and result.stdout.strip()
1272
-
1273
- await self.sio.emit('file_tracked_response', {
1274
- 'success': True,
1275
- 'file_path': file_path,
1276
- 'working_dir': working_dir,
1277
- 'is_tracked': bool(is_tracked)
1278
- }, room=sid)
1279
-
1280
- except Exception as e:
1281
- self.logger.error(f"Error checking file tracked status: {e}")
1282
- await self.sio.emit('file_tracked_response', {
1283
- 'success': False,
1284
- 'error': str(e),
1285
- 'file_path': data.get('file_path', 'unknown')
1286
- }, room=sid)
1287
-
1288
- @self.sio.event
1289
- async def read_file(sid, data):
1290
- """Read file contents safely"""
1291
- try:
1292
- file_path = data.get('file_path')
1293
- working_dir = data.get('working_dir', os.getcwd())
1294
- max_size = data.get('max_size', SystemLimits.MAX_FILE_SIZE)
1295
-
1296
- if not file_path:
1297
- await self.sio.emit('file_content_response', {
1298
- 'success': False,
1299
- 'error': 'file_path is required',
1300
- 'file_path': file_path
1301
- }, room=sid)
1302
- return
1303
-
1304
- # Use the shared file reading logic
1305
- result = await self._read_file_safely(file_path, working_dir, max_size)
1306
-
1307
- # Send the result back to the client
1308
- await self.sio.emit('file_content_response', result, room=sid)
1309
-
1310
- except Exception as e:
1311
- self.logger.error(f"Error reading file: {e}")
1312
- await self.sio.emit('file_content_response', {
1313
- 'success': False,
1314
- 'error': str(e),
1315
- 'file_path': data.get('file_path', 'unknown')
1316
- }, room=sid)
1317
-
1318
- @self.sio.event
1319
- async def check_git_status(sid, data):
1320
- """Check git status for a file to determine if git diff icons should be shown"""
1321
- import subprocess
1322
- try:
1323
- file_path = data.get('file_path')
1324
- working_dir = data.get('working_dir', os.getcwd())
1325
-
1326
- self.logger.info(f"[GIT-STATUS-DEBUG] check_git_status called with file_path: {repr(file_path)}, working_dir: {repr(working_dir)}")
1327
-
1328
- if not file_path:
1329
- await self.sio.emit('git_status_response', {
1330
- 'success': False,
1331
- 'error': 'file_path is required',
1332
- 'file_path': file_path
1333
- }, room=sid)
1334
- return
1335
-
1336
- # Validate and sanitize working_dir
1337
- original_working_dir = working_dir
1338
- if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '' or working_dir == '.':
1339
- working_dir = os.getcwd()
1340
- self.logger.info(f"[GIT-STATUS-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
1341
- else:
1342
- self.logger.info(f"[GIT-STATUS-DEBUG] Using provided working_dir: {working_dir}")
1343
-
1344
- # Check if the working directory exists and is a directory
1345
- if not os.path.exists(working_dir):
1346
- self.logger.warning(f"[GIT-STATUS-DEBUG] Directory does not exist: {working_dir}")
1347
- await self.sio.emit('git_status_response', {
1348
- 'success': False,
1349
- 'error': f'Directory does not exist: {working_dir}',
1350
- 'file_path': file_path,
1351
- 'working_dir': working_dir,
1352
- 'original_working_dir': original_working_dir
1353
- }, room=sid)
1354
- return
1355
-
1356
- if not os.path.isdir(working_dir):
1357
- self.logger.warning(f"[GIT-STATUS-DEBUG] Path is not a directory: {working_dir}")
1358
- await self.sio.emit('git_status_response', {
1359
- 'success': False,
1360
- 'error': f'Path is not a directory: {working_dir}',
1361
- 'file_path': file_path,
1362
- 'working_dir': working_dir,
1363
- 'original_working_dir': original_working_dir
1364
- }, room=sid)
1365
- return
1366
-
1367
- # Check if this is a git repository
1368
- self.logger.info(f"[GIT-STATUS-DEBUG] Checking if {working_dir} is a git repository")
1369
- git_check = subprocess.run(
1370
- ["git", "-C", working_dir, "rev-parse", "--git-dir"],
1371
- capture_output=True,
1372
- text=True
1373
- )
1374
-
1375
- if git_check.returncode != 0:
1376
- self.logger.info(f"[GIT-STATUS-DEBUG] Not a git repository: {working_dir}")
1377
- await self.sio.emit('git_status_response', {
1378
- 'success': False,
1379
- 'error': 'Not a git repository',
1380
- 'file_path': file_path,
1381
- 'working_dir': working_dir,
1382
- 'original_working_dir': original_working_dir
1383
- }, room=sid)
1384
- return
1385
-
1386
- # Determine if the file path should be made relative to git root
1387
- file_path_for_git = file_path
1388
- if os.path.isabs(file_path):
1389
- # Get git root to make path relative if needed
1390
- git_root_result = subprocess.run(
1391
- ["git", "-C", working_dir, "rev-parse", "--show-toplevel"],
1392
- capture_output=True,
1393
- text=True
1394
- )
1395
-
1396
- if git_root_result.returncode == 0:
1397
- git_root = git_root_result.stdout.strip()
1398
- try:
1399
- file_path_for_git = os.path.relpath(file_path, git_root)
1400
- self.logger.info(f"[GIT-STATUS-DEBUG] Made file path relative to git root: {file_path_for_git}")
1401
- except ValueError:
1402
- # File is not under git root - keep original path
1403
- self.logger.info(f"[GIT-STATUS-DEBUG] File not under git root, keeping original path: {file_path}")
1404
- pass
1405
-
1406
- # Check if the file exists
1407
- full_path = file_path if os.path.isabs(file_path) else os.path.join(working_dir, file_path)
1408
- if not os.path.exists(full_path):
1409
- self.logger.warning(f"[GIT-STATUS-DEBUG] File does not exist: {full_path}")
1410
- await self.sio.emit('git_status_response', {
1411
- 'success': False,
1412
- 'error': f'File does not exist: {file_path}',
1413
- 'file_path': file_path,
1414
- 'working_dir': working_dir,
1415
- 'original_working_dir': original_working_dir
1416
- }, room=sid)
1417
- return
1418
-
1419
- # Check git status for the file - this succeeds if git knows about the file
1420
- # (either tracked, modified, staged, etc.)
1421
- self.logger.info(f"[GIT-STATUS-DEBUG] Checking git status for file: {file_path_for_git}")
1422
- git_status_result = subprocess.run(
1423
- ["git", "-C", working_dir, "status", "--porcelain", file_path_for_git],
1424
- capture_output=True,
1425
- text=True
1426
- )
1427
-
1428
- self.logger.info(f"[GIT-STATUS-DEBUG] Git status result: returncode={git_status_result.returncode}, stdout={repr(git_status_result.stdout)}, stderr={repr(git_status_result.stderr)}")
1429
-
1430
- # Also check if file is tracked by git (alternative approach)
1431
- ls_files_result = subprocess.run(
1432
- ["git", "-C", working_dir, "ls-files", file_path_for_git],
1433
- capture_output=True,
1434
- text=True
1435
- )
1436
-
1437
- is_tracked = ls_files_result.returncode == 0 and ls_files_result.stdout.strip()
1438
- has_status = git_status_result.returncode == 0
1439
-
1440
- self.logger.info(f"[GIT-STATUS-DEBUG] File tracking status: is_tracked={is_tracked}, has_status={has_status}")
1441
-
1442
- # Success if git knows about the file (either tracked or has status changes)
1443
- if is_tracked or has_status:
1444
- self.logger.info(f"[GIT-STATUS-DEBUG] Git status check successful for {file_path}")
1445
- await self.sio.emit('git_status_response', {
1446
- 'success': True,
1447
- 'file_path': file_path,
1448
- 'working_dir': working_dir,
1449
- 'original_working_dir': original_working_dir,
1450
- 'is_tracked': is_tracked,
1451
- 'has_changes': bool(git_status_result.stdout.strip()) if has_status else False
1452
- }, room=sid)
1453
- else:
1454
- self.logger.info(f"[GIT-STATUS-DEBUG] File {file_path} is not tracked by git")
1455
- await self.sio.emit('git_status_response', {
1456
- 'success': False,
1457
- 'error': 'File is not tracked by git',
1458
- 'file_path': file_path,
1459
- 'working_dir': working_dir,
1460
- 'original_working_dir': original_working_dir,
1461
- 'is_tracked': False
1462
- }, room=sid)
1463
-
1464
- except Exception as e:
1465
- self.logger.error(f"[GIT-STATUS-DEBUG] Exception in check_git_status: {e}")
1466
- import traceback
1467
- self.logger.error(f"[GIT-STATUS-DEBUG] Stack trace: {traceback.format_exc()}")
1468
- await self.sio.emit('git_status_response', {
1469
- 'success': False,
1470
- 'error': str(e),
1471
- 'file_path': data.get('file_path', 'unknown'),
1472
- 'working_dir': data.get('working_dir', 'unknown')
1473
- }, room=sid)
1474
-
1475
- @self.sio.event
1476
- async def git_add_file(sid, data):
1477
- """Add file to git tracking"""
1478
- import subprocess
1479
- try:
1480
- file_path = data.get('file_path')
1481
- working_dir = data.get('working_dir', os.getcwd())
1482
-
1483
- self.logger.info(f"[GIT-ADD-DEBUG] git_add_file called with file_path: {repr(file_path)}, working_dir: {repr(working_dir)} (type: {type(working_dir)})")
1484
-
1485
- if not file_path:
1486
- await self.sio.emit('git_add_response', {
1487
- 'success': False,
1488
- 'error': 'file_path is required',
1489
- 'file_path': file_path
1490
- }, room=sid)
1491
- return
1492
-
1493
- # Validate and sanitize working_dir
1494
- original_working_dir = working_dir
1495
- if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '' or working_dir == '.':
1496
- working_dir = os.getcwd()
1497
- self.logger.info(f"[GIT-ADD-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
1498
- else:
1499
- self.logger.info(f"[GIT-ADD-DEBUG] Using provided working_dir: {working_dir}")
1500
-
1501
- # Validate that the directory exists and is a valid path
1502
- if not os.path.exists(working_dir):
1503
- self.logger.warning(f"[GIT-ADD-DEBUG] Directory does not exist: {working_dir}")
1504
- await self.sio.emit('git_add_response', {
1505
- 'success': False,
1506
- 'error': f'Directory does not exist: {working_dir}',
1507
- 'file_path': file_path,
1508
- 'working_dir': working_dir,
1509
- 'original_working_dir': original_working_dir
1510
- }, room=sid)
1511
- return
1512
-
1513
- if not os.path.isdir(working_dir):
1514
- self.logger.warning(f"[GIT-ADD-DEBUG] Path is not a directory: {working_dir}")
1515
- await self.sio.emit('git_add_response', {
1516
- 'success': False,
1517
- 'error': f'Path is not a directory: {working_dir}',
1518
- 'file_path': file_path,
1519
- 'working_dir': working_dir,
1520
- 'original_working_dir': original_working_dir
1521
- }, room=sid)
1522
- return
1523
-
1524
- self.logger.info(f"[GIT-ADD-DEBUG] Running git add command in directory: {working_dir}")
1525
-
1526
- # Use git add to track the file
1527
- result = subprocess.run(
1528
- ["git", "-C", working_dir, "add", file_path],
1529
- capture_output=True,
1530
- text=True
1531
- )
1532
-
1533
- self.logger.info(f"[GIT-ADD-DEBUG] Git add result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
1534
-
1535
- if result.returncode == 0:
1536
- self.logger.info(f"[GIT-ADD-DEBUG] Successfully added {file_path} to git in {working_dir}")
1537
- await self.sio.emit('git_add_response', {
1538
- 'success': True,
1539
- 'file_path': file_path,
1540
- 'working_dir': working_dir,
1541
- 'original_working_dir': original_working_dir,
1542
- 'message': 'File successfully added to git tracking'
1543
- }, room=sid)
1544
- else:
1545
- error_message = result.stderr.strip() or 'Unknown git error'
1546
- self.logger.warning(f"[GIT-ADD-DEBUG] Git add failed: {error_message}")
1547
- await self.sio.emit('git_add_response', {
1548
- 'success': False,
1549
- 'error': f'Git add failed: {error_message}',
1550
- 'file_path': file_path,
1551
- 'working_dir': working_dir,
1552
- 'original_working_dir': original_working_dir
1553
- }, room=sid)
1554
-
1555
- except Exception as e:
1556
- self.logger.error(f"[GIT-ADD-DEBUG] Exception in git_add_file: {e}")
1557
- import traceback
1558
- self.logger.error(f"[GIT-ADD-DEBUG] Stack trace: {traceback.format_exc()}")
1559
- await self.sio.emit('git_add_response', {
1560
- 'success': False,
1561
- 'error': str(e),
1562
- 'file_path': data.get('file_path', 'unknown'),
1563
- 'working_dir': data.get('working_dir', 'unknown')
1564
- }, room=sid)
1565
-
1566
- async def _send_current_status(self, sid: str):
1567
- """Send current system status to a client."""
1568
- try:
1569
- status = {
1570
- "type": "system.status",
1571
- "timestamp": datetime.utcnow().isoformat() + "Z",
1572
- "data": {
1573
- "session_id": self.session_id,
1574
- "session_start": self.session_start,
1575
- "claude_status": self.claude_status,
1576
- "claude_pid": self.claude_pid,
1577
- "connected_clients": len(self.clients),
1578
- "websocket_port": self.port,
1579
- "instance_info": {
1580
- "port": self.port,
1581
- "host": self.host,
1582
- "working_dir": os.getcwd() if self.session_id else None
1583
- }
1584
- }
1585
- }
1586
- await self.sio.emit('claude_event', status, room=sid)
1587
- self.logger.debug("Sent status to client")
1588
- except Exception as e:
1589
- self.logger.error(f"Failed to send status to client: {e}")
1590
- raise
1591
-
1592
- async def _send_event_history(self, sid: str, event_types: list = None, limit: int = 50):
1593
- """Send event history to a specific client.
1594
-
1595
- WHY: When clients connect to the dashboard, they need context from recent events
1596
- to understand what's been happening. This sends the most recent events in
1597
- chronological order (oldest first) so the dashboard displays them properly.
1598
-
1599
- Args:
1600
- sid: Socket.IO session ID of the client
1601
- event_types: Optional list of event types to filter by
1602
- limit: Maximum number of events to send (default: 50)
1603
- """
1604
- try:
1605
- if not self.event_history:
1606
- self.logger.debug(f"No event history to send to client {sid}")
1607
- return
1608
-
1609
- # Limit to reasonable number to avoid overwhelming client
1610
- limit = min(limit, 100)
1611
-
1612
- # Get the most recent events, filtered by type if specified
1613
- history = []
1614
- for event in reversed(self.event_history):
1615
- if not event_types or event.get("type") in event_types:
1616
- history.append(event)
1617
- if len(history) >= limit:
1618
- break
1619
-
1620
- # Reverse to get chronological order (oldest first)
1621
- history = list(reversed(history))
1622
-
1623
- if history:
1624
- # Send as 'history' event that the client expects
1625
- await self.sio.emit('history', {
1626
- "events": history,
1627
- "count": len(history),
1628
- "total_available": len(self.event_history)
1629
- }, room=sid)
1630
-
1631
- self.logger.info(f"📚 Sent {len(history)} historical events to client {sid}")
1632
- else:
1633
- self.logger.debug(f"No matching events found for client {sid} with filters: {event_types}")
1634
-
1635
- except Exception as e:
1636
- self.logger.error(f"❌ Failed to send event history to client {sid}: {e}")
1637
- import traceback
1638
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
1639
-
1640
- def broadcast_event(self, event_type: str, data: Dict[str, Any]):
1641
- """Broadcast an event to all connected clients."""
1642
- if not SOCKETIO_AVAILABLE:
1643
- self.logger.debug(f"⚠️ Socket.IO broadcast skipped - packages not available")
1644
- return
1645
-
1646
- event = {
1647
- "type": event_type,
1648
- "timestamp": datetime.utcnow().isoformat() + "Z",
1649
- "data": data
1650
- }
1651
-
1652
- self.logger.info(f"📤 BROADCASTING EVENT: {event_type}")
1653
- self.logger.debug(f"📄 Event data: {json.dumps(data, indent=2)[:200]}...")
1654
-
1655
- # Store in history
1656
- self.event_history.append(event)
1657
- self.logger.debug(f"📚 Event stored in history (total: {len(self.event_history)})")
1658
-
1659
- # Check if we have clients and event loop
1660
- if not self.clients:
1661
- self.logger.warning(f"⚠️ No Socket.IO clients connected - event will not be delivered")
1662
- return
1663
-
1664
- if not self.loop or not self.sio:
1665
- self.logger.error(f"❌ No event loop or Socket.IO instance available - cannot broadcast event")
1666
- return
1667
-
1668
- self.logger.info(f"🎯 Broadcasting to {len(self.clients)} clients via event loop")
1669
-
1670
- # Broadcast to clients with timeout and error handling
1671
- try:
1672
- # Check if the event loop is still running and not closed
1673
- if self.loop and not self.loop.is_closed() and self.loop.is_running():
1674
- future = asyncio.run_coroutine_threadsafe(
1675
- self.sio.emit('claude_event', event),
1676
- self.loop
1677
- )
1678
- # Wait for completion with timeout to detect issues
1679
- try:
1680
- future.result(timeout=TimeoutConfig.QUICK_TIMEOUT)
1681
- self.logger.debug(f"📨 Successfully broadcasted {event_type} to {len(self.clients)} clients")
1682
- except asyncio.TimeoutError:
1683
- self.logger.warning(f"⏰ Broadcast timeout for event {event_type} - continuing anyway")
1684
- except Exception as emit_error:
1685
- self.logger.error(f"❌ Broadcast emit error for {event_type}: {emit_error}")
1686
- else:
1687
- self.logger.warning(f"⚠️ Event loop not available for broadcast of {event_type} - event loop closed or not running")
1688
- except Exception as e:
1689
- self.logger.error(f"❌ Failed to submit broadcast to event loop: {e}")
1690
- import traceback
1691
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
1692
-
1693
- # Convenience methods for common events (same interface as WebSocketServer)
1694
-
1695
- def session_started(self, session_id: str, launch_method: str, working_dir: str):
1696
- """Notify that a session has started."""
1697
- self.session_id = session_id
1698
- self.session_start = datetime.utcnow().isoformat() + "Z"
1699
- self.broadcast_event("session.start", {
1700
- "session_id": session_id,
1701
- "start_time": self.session_start,
1702
- "launch_method": launch_method,
1703
- "working_directory": working_dir,
1704
- "websocket_port": self.port,
1705
- "instance_info": {
1706
- "port": self.port,
1707
- "host": self.host,
1708
- "working_dir": working_dir
1709
- }
1710
- })
1711
-
1712
- def session_ended(self):
1713
- """Notify that a session has ended."""
1714
- if self.session_id:
1715
- duration = None
1716
- if self.session_start:
1717
- start = datetime.fromisoformat(self.session_start.replace("Z", "+00:00"))
1718
- duration = (datetime.utcnow() - start.replace(tzinfo=None)).total_seconds()
1719
-
1720
- self.broadcast_event("session.end", {
1721
- "session_id": self.session_id,
1722
- "end_time": datetime.utcnow().isoformat() + "Z",
1723
- "duration_seconds": duration
1724
- })
1725
-
1726
- self.session_id = None
1727
- self.session_start = None
1728
-
1729
- def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
1730
- """Notify Claude status change."""
1731
- self.claude_status = status
1732
- self.claude_pid = pid
1733
- self.broadcast_event("claude.status", {
1734
- "status": status,
1735
- "pid": pid,
1736
- "message": message
1737
- })
1738
-
1739
- def claude_output(self, content: str, stream: str = "stdout"):
1740
- """Broadcast Claude output."""
1741
- self.broadcast_event("claude.output", {
1742
- "content": content,
1743
- "stream": stream
1744
- })
1745
-
1746
- def agent_delegated(self, agent: str, task: str, status: str = "started"):
1747
- """Notify agent delegation."""
1748
- self.broadcast_event("agent.delegation", {
1749
- "agent": agent,
1750
- "task": task,
1751
- "status": status,
1752
- "timestamp": datetime.utcnow().isoformat() + "Z"
1753
- })
1754
-
1755
- def todo_updated(self, todos: List[Dict[str, Any]]):
1756
- """Notify todo list update."""
1757
- stats = {
1758
- "total": len(todos),
1759
- "completed": sum(1 for t in todos if t.get("status") == "completed"),
1760
- "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
1761
- "pending": sum(1 for t in todos if t.get("status") == "pending")
1762
- }
1763
-
1764
- self.broadcast_event("todo.update", {
1765
- "todos": todos,
1766
- "stats": stats
1767
- })
1768
-
1769
- def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
1770
- """Notify ticket creation."""
1771
- self.broadcast_event("ticket.created", {
1772
- "id": ticket_id,
1773
- "title": title,
1774
- "priority": priority,
1775
- "created_at": datetime.utcnow().isoformat() + "Z"
1776
- })
1777
-
1778
- def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
1779
- """Notify when agent memory is loaded from file."""
1780
- self.broadcast_event("memory:loaded", {
1781
- "agent_id": agent_id,
1782
- "memory_size": memory_size,
1783
- "sections_count": sections_count,
1784
- "timestamp": datetime.utcnow().isoformat() + "Z"
1785
- })
1786
-
1787
- def memory_created(self, agent_id: str, template_type: str):
1788
- """Notify when new agent memory is created from template."""
1789
- self.broadcast_event("memory:created", {
1790
- "agent_id": agent_id,
1791
- "template_type": template_type,
1792
- "timestamp": datetime.utcnow().isoformat() + "Z"
1793
- })
1794
-
1795
- def memory_updated(self, agent_id: str, learning_type: str, content: str, section: str):
1796
- """Notify when learning is added to agent memory."""
1797
- self.broadcast_event("memory:updated", {
1798
- "agent_id": agent_id,
1799
- "learning_type": learning_type,
1800
- "content": content,
1801
- "section": section,
1802
- "timestamp": datetime.utcnow().isoformat() + "Z"
1803
- })
1804
-
1805
- def memory_injected(self, agent_id: str, context_size: int):
1806
- """Notify when agent memory is injected into context."""
1807
- self.broadcast_event("memory:injected", {
1808
- "agent_id": agent_id,
1809
- "context_size": context_size,
1810
- "timestamp": datetime.utcnow().isoformat() + "Z"
1811
- })
1812
-
1813
- # ================================================================================
1814
- # Interface Adapter Methods
1815
- # ================================================================================
1816
- # These methods adapt the existing implementation to comply with SocketIOServiceInterface
1817
-
1818
- async def start(self, host: str = "localhost", port: int = 8765) -> None:
1819
- """Start the WebSocket server (async adapter).
1820
-
1821
- WHY: The interface expects async methods, but our implementation uses
1822
- synchronous start with background threads. This adapter provides compatibility.
1823
-
1824
- Args:
1825
- host: Host to bind to
1826
- port: Port to listen on
1827
- """
1828
- self.host = host
1829
- self.port = port
1830
- # Call the synchronous start method
1831
- self.start_sync()
1832
-
1833
- async def stop(self) -> None:
1834
- """Stop the WebSocket server (async adapter).
1835
-
1836
- WHY: The interface expects async methods. This adapter wraps the
1837
- synchronous stop method for interface compliance.
1838
- """
1839
- # Call the synchronous stop method
1840
- self.stop_sync()
1841
-
1842
- async def emit(self, event: str, data: Any, room: Optional[str] = None) -> None:
1843
- """Emit an event to connected clients.
1844
-
1845
- WHY: Provides interface compliance by wrapping broadcast_event with
1846
- async signature and room support.
1847
-
1848
- Args:
1849
- event: Event name
1850
- data: Event data
1851
- room: Optional room to target (not supported in current implementation)
1852
- """
1853
- if room:
1854
- self.logger.warning(f"Room-based emit not supported, broadcasting to all: {event}")
1855
-
1856
- # Use existing broadcast_event method
1857
- self.broadcast_event(event, data)
1858
-
1859
- async def broadcast(self, event: str, data: Any) -> None:
1860
- """Broadcast event to all connected clients.
1861
-
1862
- WHY: Provides interface compliance with async signature.
1863
-
1864
- Args:
1865
- event: Event name
1866
- data: Event data
1867
- """
1868
- self.broadcast_event(event, data)
1869
-
1870
- def get_connection_count(self) -> int:
1871
- """Get number of connected clients.
1872
-
1873
- WHY: Provides interface compliance for monitoring connections.
1874
-
1875
- Returns:
1876
- Number of active connections
1877
- """
1878
- return len(self.clients)
1879
-
1880
- def is_running(self) -> bool:
1881
- """Check if server is running.
1882
-
1883
- WHY: Provides interface compliance for status checking.
1884
-
1885
- Returns:
1886
- True if server is active
1887
- """
1888
- return self.running
1889
-
1890
-
1891
- # Global instance for easy access
1892
- _socketio_server: Optional[SocketIOServer] = None
1893
-
1894
-
1895
- def get_socketio_server() -> SocketIOServer:
1896
- """Get or create the global Socket.IO server instance.
1897
-
1898
- WHY: In exec mode, a persistent Socket.IO server may already be running
1899
- in a separate process. We need to detect this and create a client proxy
1900
- instead of trying to start another server.
1901
- """
1902
- global _socketio_server
1903
- if _socketio_server is None:
1904
- # Check if a Socket.IO server is already running on the default port
1905
- import socket
1906
- try:
1907
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1908
- s.settimeout(0.5)
1909
- result = s.connect_ex(('127.0.0.1', 8765))
1910
- if result == 0:
1911
- # Server is already running - create a client proxy
1912
- _socketio_server = SocketIOClientProxy(port=8765)
1913
- else:
1914
- # No server running - create a real server
1915
- _socketio_server = SocketIOServer()
1916
- except Exception:
1917
- # On any error, create a real server
1918
- _socketio_server = SocketIOServer()
1919
-
1920
- return _socketio_server
1921
-
1922
-
1923
- def start_socketio_server():
1924
- """Start the global Socket.IO server."""
1925
- server = get_socketio_server()
1926
- server.start_sync()
1927
- return server
1928
-
1929
-
1930
- def stop_socketio_server():
1931
- """Stop the global Socket.IO server."""
1932
- global _socketio_server
1933
- if _socketio_server:
1934
- _socketio_server.stop_sync()
1935
- _socketio_server = None