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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +2 -2
- claude_mpm/__main__.py +3 -2
- claude_mpm/agents/__init__.py +85 -79
- claude_mpm/agents/agent_loader.py +464 -1003
- claude_mpm/agents/agent_loader_integration.py +45 -45
- claude_mpm/agents/agents_metadata.py +29 -30
- claude_mpm/agents/async_agent_loader.py +156 -138
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/base_agent_loader.py +179 -151
- claude_mpm/agents/frontmatter_validator.py +229 -130
- claude_mpm/agents/schema/agent_schema.json +1 -1
- claude_mpm/agents/system_agent_config.py +213 -147
- claude_mpm/agents/templates/__init__.py +13 -13
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +23 -11
- claude_mpm/agents/templates/engineer.json +22 -6
- claude_mpm/agents/templates/memory_manager.json +155 -0
- claude_mpm/agents/templates/ops.json +2 -2
- claude_mpm/agents/templates/project_organizer.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/refactoring_engineer.json +222 -0
- claude_mpm/agents/templates/research.json +20 -14
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +3 -1
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +90 -49
- claude_mpm/cli/__main__.py +3 -2
- claude_mpm/cli/commands/__init__.py +21 -18
- claude_mpm/cli/commands/agents.py +279 -247
- claude_mpm/cli/commands/aggregate.py +138 -157
- claude_mpm/cli/commands/cleanup.py +147 -147
- claude_mpm/cli/commands/config.py +93 -76
- claude_mpm/cli/commands/info.py +17 -16
- claude_mpm/cli/commands/mcp.py +143 -762
- claude_mpm/cli/commands/mcp_command_router.py +139 -0
- claude_mpm/cli/commands/mcp_config_commands.py +20 -0
- claude_mpm/cli/commands/mcp_install_commands.py +20 -0
- claude_mpm/cli/commands/mcp_server_commands.py +175 -0
- claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
- claude_mpm/cli/commands/memory.py +239 -203
- claude_mpm/cli/commands/monitor.py +203 -81
- claude_mpm/cli/commands/run.py +380 -429
- claude_mpm/cli/commands/run_config_checker.py +160 -0
- claude_mpm/cli/commands/socketio_monitor.py +235 -0
- claude_mpm/cli/commands/tickets.py +305 -197
- claude_mpm/cli/parser.py +24 -1150
- claude_mpm/cli/parsers/__init__.py +29 -0
- claude_mpm/cli/parsers/agents_parser.py +136 -0
- claude_mpm/cli/parsers/base_parser.py +331 -0
- claude_mpm/cli/parsers/config_parser.py +85 -0
- claude_mpm/cli/parsers/mcp_parser.py +152 -0
- claude_mpm/cli/parsers/memory_parser.py +138 -0
- claude_mpm/cli/parsers/monitor_parser.py +104 -0
- claude_mpm/cli/parsers/run_parser.py +147 -0
- claude_mpm/cli/parsers/tickets_parser.py +203 -0
- claude_mpm/cli/ticket_cli.py +7 -3
- claude_mpm/cli/utils.py +55 -37
- claude_mpm/cli_module/__init__.py +6 -6
- claude_mpm/cli_module/args.py +188 -140
- claude_mpm/cli_module/commands.py +79 -70
- claude_mpm/cli_module/migration_example.py +38 -60
- claude_mpm/config/__init__.py +32 -25
- claude_mpm/config/agent_config.py +151 -119
- claude_mpm/config/experimental_features.py +217 -0
- claude_mpm/config/paths.py +94 -208
- claude_mpm/config/socketio_config.py +84 -73
- claude_mpm/constants.py +36 -18
- claude_mpm/core/__init__.py +9 -6
- claude_mpm/core/agent_name_normalizer.py +68 -71
- claude_mpm/core/agent_registry.py +372 -521
- claude_mpm/core/agent_session_manager.py +74 -63
- claude_mpm/core/base_service.py +116 -87
- claude_mpm/core/cache.py +119 -153
- claude_mpm/core/claude_runner.py +425 -1120
- claude_mpm/core/config.py +263 -168
- claude_mpm/core/config_aliases.py +69 -61
- claude_mpm/core/config_constants.py +292 -0
- claude_mpm/core/constants.py +57 -99
- claude_mpm/core/container.py +211 -178
- claude_mpm/core/exceptions.py +233 -89
- claude_mpm/core/factories.py +92 -54
- claude_mpm/core/framework_loader.py +378 -220
- claude_mpm/core/hook_manager.py +198 -83
- claude_mpm/core/hook_performance_config.py +136 -0
- claude_mpm/core/injectable_service.py +61 -55
- claude_mpm/core/interactive_session.py +165 -155
- claude_mpm/core/interfaces.py +221 -195
- claude_mpm/core/lazy.py +96 -96
- claude_mpm/core/logger.py +133 -107
- claude_mpm/core/logging_config.py +185 -157
- claude_mpm/core/minimal_framework_loader.py +20 -15
- claude_mpm/core/mixins.py +30 -29
- claude_mpm/core/oneshot_session.py +215 -181
- claude_mpm/core/optimized_agent_loader.py +134 -138
- claude_mpm/core/optimized_startup.py +159 -157
- claude_mpm/core/pm_hook_interceptor.py +85 -72
- claude_mpm/core/service_registry.py +103 -101
- claude_mpm/core/session_manager.py +97 -87
- claude_mpm/core/socketio_pool.py +212 -158
- claude_mpm/core/tool_access_control.py +58 -51
- claude_mpm/core/types.py +46 -24
- claude_mpm/core/typing_utils.py +166 -82
- claude_mpm/core/unified_agent_registry.py +721 -0
- claude_mpm/core/unified_config.py +550 -0
- claude_mpm/core/unified_paths.py +549 -0
- claude_mpm/dashboard/index.html +1 -1
- claude_mpm/dashboard/open_dashboard.py +51 -17
- claude_mpm/dashboard/static/css/dashboard.css +27 -8
- claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
- claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
- claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
- claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
- claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
- claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
- claude_mpm/dashboard/static/dist/dashboard.js +2 -0
- claude_mpm/dashboard/static/dist/socket-client.js +2 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
- claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
- claude_mpm/dashboard/static/js/components/event-viewer.js +74 -70
- claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +106 -92
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
- claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
- claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
- claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
- claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
- claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
- claude_mpm/dashboard/static/js/dashboard.js +178 -453
- claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
- claude_mpm/dashboard/static/js/socket-client.js +120 -54
- claude_mpm/dashboard/templates/index.html +40 -50
- claude_mpm/experimental/cli_enhancements.py +60 -58
- claude_mpm/generators/__init__.py +1 -1
- claude_mpm/generators/agent_profile_generator.py +75 -65
- claude_mpm/hooks/__init__.py +1 -1
- claude_mpm/hooks/base_hook.py +33 -28
- claude_mpm/hooks/claude_hooks/__init__.py +1 -1
- claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
- claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
- claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
- claude_mpm/hooks/memory_integration_hook.py +140 -100
- claude_mpm/hooks/tool_call_interceptor.py +89 -76
- claude_mpm/hooks/validation_hooks.py +57 -49
- claude_mpm/init.py +145 -121
- claude_mpm/models/__init__.py +9 -9
- claude_mpm/models/agent_definition.py +33 -23
- claude_mpm/models/agent_session.py +228 -200
- claude_mpm/scripts/__init__.py +1 -1
- claude_mpm/scripts/socketio_daemon.py +192 -75
- claude_mpm/scripts/socketio_server_manager.py +328 -0
- claude_mpm/scripts/start_activity_logging.py +25 -22
- claude_mpm/services/__init__.py +68 -43
- claude_mpm/services/agent_capabilities_service.py +271 -0
- claude_mpm/services/agents/__init__.py +23 -32
- claude_mpm/services/agents/deployment/__init__.py +3 -3
- claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
- claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
- claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
- claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
- claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
- claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
- claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
- claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
- claude_mpm/services/agents/deployment/agent_validator.py +352 -0
- claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
- claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
- claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
- claude_mpm/services/agents/deployment/config/__init__.py +13 -0
- claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
- claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
- claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
- claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
- claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
- claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
- claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
- claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
- claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
- claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
- claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
- claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
- claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
- claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
- claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
- claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
- claude_mpm/services/agents/deployment/results/__init__.py +13 -0
- claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
- claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
- claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
- claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
- claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
- claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
- claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
- claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
- claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
- claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
- claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
- claude_mpm/services/agents/loading/__init__.py +2 -2
- claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
- claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
- claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
- claude_mpm/services/agents/management/__init__.py +2 -2
- claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
- claude_mpm/services/agents/management/agent_management_service.py +209 -156
- claude_mpm/services/agents/memory/__init__.py +9 -6
- claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
- claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
- claude_mpm/services/agents/memory/analyzer.py +430 -0
- claude_mpm/services/agents/memory/content_manager.py +376 -0
- claude_mpm/services/agents/memory/template_generator.py +468 -0
- claude_mpm/services/agents/registry/__init__.py +7 -10
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
- claude_mpm/services/agents/registry/modification_tracker.py +351 -285
- claude_mpm/services/async_session_logger.py +187 -153
- claude_mpm/services/claude_session_logger.py +87 -72
- claude_mpm/services/command_handler_service.py +217 -0
- claude_mpm/services/communication/__init__.py +3 -2
- claude_mpm/services/core/__init__.py +50 -97
- claude_mpm/services/core/base.py +60 -53
- claude_mpm/services/core/interfaces/__init__.py +188 -0
- claude_mpm/services/core/interfaces/agent.py +351 -0
- claude_mpm/services/core/interfaces/communication.py +343 -0
- claude_mpm/services/core/interfaces/infrastructure.py +413 -0
- claude_mpm/services/core/interfaces/service.py +434 -0
- claude_mpm/services/core/interfaces.py +19 -944
- claude_mpm/services/event_aggregator.py +208 -170
- claude_mpm/services/exceptions.py +387 -308
- claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
- claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
- claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
- claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
- claude_mpm/services/hook_service.py +106 -114
- claude_mpm/services/infrastructure/__init__.py +7 -5
- claude_mpm/services/infrastructure/context_preservation.py +571 -0
- claude_mpm/services/infrastructure/daemon_manager.py +279 -0
- claude_mpm/services/infrastructure/logging.py +83 -76
- claude_mpm/services/infrastructure/monitoring.py +547 -404
- claude_mpm/services/mcp_gateway/__init__.py +40 -23
- claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
- claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
- claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
- claude_mpm/services/mcp_gateway/core/__init__.py +14 -21
- claude_mpm/services/mcp_gateway/core/base.py +80 -67
- claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
- claude_mpm/services/mcp_gateway/core/interfaces.py +97 -93
- claude_mpm/services/mcp_gateway/main.py +307 -127
- claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
- claude_mpm/services/mcp_gateway/registry/service_registry.py +100 -101
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
- claude_mpm/services/mcp_gateway/server/__init__.py +4 -4
- claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +149 -153
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
- claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
- claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +110 -121
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
- claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
- claude_mpm/services/memory/__init__.py +2 -2
- claude_mpm/services/memory/builder.py +451 -362
- claude_mpm/services/memory/cache/__init__.py +2 -2
- claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
- claude_mpm/services/memory/cache/simple_cache.py +107 -93
- claude_mpm/services/memory/indexed_memory.py +195 -193
- claude_mpm/services/memory/optimizer.py +267 -234
- claude_mpm/services/memory/router.py +571 -263
- claude_mpm/services/memory_hook_service.py +237 -0
- claude_mpm/services/port_manager.py +223 -0
- claude_mpm/services/project/__init__.py +3 -3
- claude_mpm/services/project/analyzer.py +451 -305
- claude_mpm/services/project/registry.py +262 -240
- claude_mpm/services/recovery_manager.py +287 -231
- claude_mpm/services/response_tracker.py +87 -67
- claude_mpm/services/runner_configuration_service.py +587 -0
- claude_mpm/services/session_management_service.py +304 -0
- claude_mpm/services/socketio/__init__.py +4 -4
- claude_mpm/services/socketio/client_proxy.py +174 -0
- claude_mpm/services/socketio/handlers/__init__.py +3 -3
- claude_mpm/services/socketio/handlers/base.py +44 -30
- claude_mpm/services/socketio/handlers/connection.py +145 -65
- claude_mpm/services/socketio/handlers/file.py +123 -108
- claude_mpm/services/socketio/handlers/git.py +607 -373
- claude_mpm/services/socketio/handlers/hook.py +170 -0
- claude_mpm/services/socketio/handlers/memory.py +4 -4
- claude_mpm/services/socketio/handlers/project.py +4 -4
- claude_mpm/services/socketio/handlers/registry.py +53 -38
- claude_mpm/services/socketio/server/__init__.py +18 -0
- claude_mpm/services/socketio/server/broadcaster.py +252 -0
- claude_mpm/services/socketio/server/core.py +399 -0
- claude_mpm/services/socketio/server/main.py +323 -0
- claude_mpm/services/socketio_client_manager.py +160 -133
- claude_mpm/services/socketio_server.py +36 -1885
- claude_mpm/services/subprocess_launcher_service.py +316 -0
- claude_mpm/services/system_instructions_service.py +258 -0
- claude_mpm/services/ticket_manager.py +20 -534
- claude_mpm/services/utility_service.py +285 -0
- claude_mpm/services/version_control/__init__.py +18 -21
- claude_mpm/services/version_control/branch_strategy.py +20 -10
- claude_mpm/services/version_control/conflict_resolution.py +37 -13
- claude_mpm/services/version_control/git_operations.py +52 -21
- claude_mpm/services/version_control/semantic_versioning.py +92 -53
- claude_mpm/services/version_control/version_parser.py +145 -125
- claude_mpm/services/version_service.py +270 -0
- claude_mpm/storage/__init__.py +9 -0
- claude_mpm/storage/state_storage.py +552 -0
- claude_mpm/ticket_wrapper.py +2 -2
- claude_mpm/utils/__init__.py +2 -2
- claude_mpm/utils/agent_dependency_loader.py +453 -243
- claude_mpm/utils/config_manager.py +157 -118
- claude_mpm/utils/console.py +1 -1
- claude_mpm/utils/dependency_cache.py +102 -107
- claude_mpm/utils/dependency_manager.py +52 -47
- claude_mpm/utils/dependency_strategies.py +131 -96
- claude_mpm/utils/environment_context.py +110 -102
- claude_mpm/utils/error_handler.py +75 -55
- claude_mpm/utils/file_utils.py +80 -67
- claude_mpm/utils/framework_detection.py +12 -11
- claude_mpm/utils/import_migration_example.py +12 -60
- claude_mpm/utils/imports.py +48 -45
- claude_mpm/utils/path_operations.py +100 -93
- claude_mpm/utils/robust_installer.py +172 -164
- claude_mpm/utils/session_logging.py +30 -23
- claude_mpm/utils/subprocess_utils.py +99 -61
- claude_mpm/validation/__init__.py +1 -1
- claude_mpm/validation/agent_validator.py +151 -111
- claude_mpm/validation/frontmatter_validator.py +92 -71
- {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/METADATA +51 -2
- claude_mpm-4.0.3.dist-info/RECORD +402 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/entry_points.txt +1 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/licenses/LICENSE +1 -1
- claude_mpm/config/memory_guardian_config.py +0 -325
- claude_mpm/core/config_paths.py +0 -150
- claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
- claude_mpm/deployment_paths.py +0 -261
- claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
- claude_mpm/models/state_models.py +0 -433
- claude_mpm/services/agent/__init__.py +0 -24
- claude_mpm/services/agent/deployment.py +0 -2548
- claude_mpm/services/agent/management.py +0 -598
- claude_mpm/services/agent/registry.py +0 -813
- claude_mpm/services/agents/registry/agent_registry.py +0 -813
- claude_mpm/services/communication/socketio.py +0 -1935
- claude_mpm/services/communication/websocket.py +0 -479
- claude_mpm/services/framework_claude_md_generator.py +0 -624
- claude_mpm/services/health_monitor.py +0 -893
- claude_mpm/services/infrastructure/memory_guardian.py +0 -770
- claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
- claude_mpm/services/optimized_hook_service.py +0 -542
- claude_mpm/services/project_analyzer.py +0 -864
- claude_mpm/services/project_registry.py +0 -608
- claude_mpm/services/standalone_socketio_server.py +0 -1300
- claude_mpm/services/ticket_manager_di.py +0 -318
- claude_mpm/services/ticketing_service_original.py +0 -510
- claude_mpm/utils/paths.py +0 -395
- claude_mpm/utils/platform_memory.py +0 -524
- claude_mpm-3.9.9.dist-info/RECORD +0 -293
- {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/WHEEL +0 -0
- {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
         |