claude-mpm 3.9.9__py3-none-any.whl → 4.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +2 -2
  3. claude_mpm/__main__.py +3 -2
  4. claude_mpm/agents/__init__.py +85 -79
  5. claude_mpm/agents/agent_loader.py +464 -1003
  6. claude_mpm/agents/agent_loader_integration.py +45 -45
  7. claude_mpm/agents/agents_metadata.py +29 -30
  8. claude_mpm/agents/async_agent_loader.py +156 -138
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/base_agent_loader.py +179 -151
  11. claude_mpm/agents/frontmatter_validator.py +229 -130
  12. claude_mpm/agents/schema/agent_schema.json +1 -1
  13. claude_mpm/agents/system_agent_config.py +213 -147
  14. claude_mpm/agents/templates/__init__.py +13 -13
  15. claude_mpm/agents/templates/code_analyzer.json +2 -2
  16. claude_mpm/agents/templates/data_engineer.json +1 -1
  17. claude_mpm/agents/templates/documentation.json +23 -11
  18. claude_mpm/agents/templates/engineer.json +22 -6
  19. claude_mpm/agents/templates/memory_manager.json +155 -0
  20. claude_mpm/agents/templates/ops.json +2 -2
  21. claude_mpm/agents/templates/project_organizer.json +1 -1
  22. claude_mpm/agents/templates/qa.json +1 -1
  23. claude_mpm/agents/templates/refactoring_engineer.json +222 -0
  24. claude_mpm/agents/templates/research.json +20 -14
  25. claude_mpm/agents/templates/security.json +1 -1
  26. claude_mpm/agents/templates/ticketing.json +1 -1
  27. claude_mpm/agents/templates/version_control.json +1 -1
  28. claude_mpm/agents/templates/web_qa.json +3 -1
  29. claude_mpm/agents/templates/web_ui.json +2 -2
  30. claude_mpm/cli/__init__.py +90 -49
  31. claude_mpm/cli/__main__.py +3 -2
  32. claude_mpm/cli/commands/__init__.py +21 -18
  33. claude_mpm/cli/commands/agents.py +279 -247
  34. claude_mpm/cli/commands/aggregate.py +138 -157
  35. claude_mpm/cli/commands/cleanup.py +147 -147
  36. claude_mpm/cli/commands/config.py +93 -76
  37. claude_mpm/cli/commands/info.py +17 -16
  38. claude_mpm/cli/commands/mcp.py +143 -762
  39. claude_mpm/cli/commands/mcp_command_router.py +139 -0
  40. claude_mpm/cli/commands/mcp_config_commands.py +20 -0
  41. claude_mpm/cli/commands/mcp_install_commands.py +20 -0
  42. claude_mpm/cli/commands/mcp_server_commands.py +175 -0
  43. claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
  44. claude_mpm/cli/commands/memory.py +239 -203
  45. claude_mpm/cli/commands/monitor.py +203 -81
  46. claude_mpm/cli/commands/run.py +380 -429
  47. claude_mpm/cli/commands/run_config_checker.py +160 -0
  48. claude_mpm/cli/commands/socketio_monitor.py +235 -0
  49. claude_mpm/cli/commands/tickets.py +305 -197
  50. claude_mpm/cli/parser.py +24 -1150
  51. claude_mpm/cli/parsers/__init__.py +29 -0
  52. claude_mpm/cli/parsers/agents_parser.py +136 -0
  53. claude_mpm/cli/parsers/base_parser.py +331 -0
  54. claude_mpm/cli/parsers/config_parser.py +85 -0
  55. claude_mpm/cli/parsers/mcp_parser.py +152 -0
  56. claude_mpm/cli/parsers/memory_parser.py +138 -0
  57. claude_mpm/cli/parsers/monitor_parser.py +104 -0
  58. claude_mpm/cli/parsers/run_parser.py +147 -0
  59. claude_mpm/cli/parsers/tickets_parser.py +203 -0
  60. claude_mpm/cli/ticket_cli.py +7 -3
  61. claude_mpm/cli/utils.py +55 -37
  62. claude_mpm/cli_module/__init__.py +6 -6
  63. claude_mpm/cli_module/args.py +188 -140
  64. claude_mpm/cli_module/commands.py +79 -70
  65. claude_mpm/cli_module/migration_example.py +38 -60
  66. claude_mpm/config/__init__.py +32 -25
  67. claude_mpm/config/agent_config.py +151 -119
  68. claude_mpm/config/experimental_features.py +217 -0
  69. claude_mpm/config/paths.py +94 -208
  70. claude_mpm/config/socketio_config.py +84 -73
  71. claude_mpm/constants.py +36 -18
  72. claude_mpm/core/__init__.py +9 -6
  73. claude_mpm/core/agent_name_normalizer.py +68 -71
  74. claude_mpm/core/agent_registry.py +372 -521
  75. claude_mpm/core/agent_session_manager.py +74 -63
  76. claude_mpm/core/base_service.py +116 -87
  77. claude_mpm/core/cache.py +119 -153
  78. claude_mpm/core/claude_runner.py +425 -1120
  79. claude_mpm/core/config.py +263 -168
  80. claude_mpm/core/config_aliases.py +69 -61
  81. claude_mpm/core/config_constants.py +292 -0
  82. claude_mpm/core/constants.py +57 -99
  83. claude_mpm/core/container.py +211 -178
  84. claude_mpm/core/exceptions.py +233 -89
  85. claude_mpm/core/factories.py +92 -54
  86. claude_mpm/core/framework_loader.py +378 -220
  87. claude_mpm/core/hook_manager.py +198 -83
  88. claude_mpm/core/hook_performance_config.py +136 -0
  89. claude_mpm/core/injectable_service.py +61 -55
  90. claude_mpm/core/interactive_session.py +165 -155
  91. claude_mpm/core/interfaces.py +221 -195
  92. claude_mpm/core/lazy.py +96 -96
  93. claude_mpm/core/logger.py +133 -107
  94. claude_mpm/core/logging_config.py +185 -157
  95. claude_mpm/core/minimal_framework_loader.py +20 -15
  96. claude_mpm/core/mixins.py +30 -29
  97. claude_mpm/core/oneshot_session.py +215 -181
  98. claude_mpm/core/optimized_agent_loader.py +134 -138
  99. claude_mpm/core/optimized_startup.py +159 -157
  100. claude_mpm/core/pm_hook_interceptor.py +85 -72
  101. claude_mpm/core/service_registry.py +103 -101
  102. claude_mpm/core/session_manager.py +97 -87
  103. claude_mpm/core/socketio_pool.py +212 -158
  104. claude_mpm/core/tool_access_control.py +58 -51
  105. claude_mpm/core/types.py +46 -24
  106. claude_mpm/core/typing_utils.py +166 -82
  107. claude_mpm/core/unified_agent_registry.py +721 -0
  108. claude_mpm/core/unified_config.py +550 -0
  109. claude_mpm/core/unified_paths.py +549 -0
  110. claude_mpm/dashboard/index.html +1 -1
  111. claude_mpm/dashboard/open_dashboard.py +51 -17
  112. claude_mpm/dashboard/static/css/dashboard.css +27 -8
  113. claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
  114. claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
  115. claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
  116. claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
  117. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
  118. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
  119. claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
  120. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
  121. claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
  122. claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
  123. claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
  124. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
  125. claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
  126. claude_mpm/dashboard/static/dist/dashboard.js +2 -0
  127. claude_mpm/dashboard/static/dist/socket-client.js +2 -0
  128. claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
  129. claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
  130. claude_mpm/dashboard/static/js/components/event-viewer.js +74 -70
  131. claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
  132. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +106 -92
  133. claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
  134. claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
  135. claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
  136. claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
  137. claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
  138. claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
  139. claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
  140. claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
  141. claude_mpm/dashboard/static/js/dashboard.js +178 -453
  142. claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
  143. claude_mpm/dashboard/static/js/socket-client.js +120 -54
  144. claude_mpm/dashboard/templates/index.html +40 -50
  145. claude_mpm/experimental/cli_enhancements.py +60 -58
  146. claude_mpm/generators/__init__.py +1 -1
  147. claude_mpm/generators/agent_profile_generator.py +75 -65
  148. claude_mpm/hooks/__init__.py +1 -1
  149. claude_mpm/hooks/base_hook.py +33 -28
  150. claude_mpm/hooks/claude_hooks/__init__.py +1 -1
  151. claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
  152. claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
  153. claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
  154. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
  155. claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
  156. claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
  157. claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
  158. claude_mpm/hooks/memory_integration_hook.py +140 -100
  159. claude_mpm/hooks/tool_call_interceptor.py +89 -76
  160. claude_mpm/hooks/validation_hooks.py +57 -49
  161. claude_mpm/init.py +145 -121
  162. claude_mpm/models/__init__.py +9 -9
  163. claude_mpm/models/agent_definition.py +33 -23
  164. claude_mpm/models/agent_session.py +228 -200
  165. claude_mpm/scripts/__init__.py +1 -1
  166. claude_mpm/scripts/socketio_daemon.py +192 -75
  167. claude_mpm/scripts/socketio_server_manager.py +328 -0
  168. claude_mpm/scripts/start_activity_logging.py +25 -22
  169. claude_mpm/services/__init__.py +68 -43
  170. claude_mpm/services/agent_capabilities_service.py +271 -0
  171. claude_mpm/services/agents/__init__.py +23 -32
  172. claude_mpm/services/agents/deployment/__init__.py +3 -3
  173. claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
  174. claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
  175. claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
  176. claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
  177. claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
  178. claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
  179. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
  180. claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
  181. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
  182. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
  183. claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
  184. claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
  185. claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
  186. claude_mpm/services/agents/deployment/agent_validator.py +352 -0
  187. claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
  188. claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
  189. claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
  190. claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
  191. claude_mpm/services/agents/deployment/config/__init__.py +13 -0
  192. claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
  193. claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
  194. claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
  195. claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
  196. claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
  197. claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
  198. claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
  199. claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
  200. claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
  201. claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
  202. claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
  203. claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
  204. claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
  205. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
  206. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
  207. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
  208. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
  209. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
  210. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
  211. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
  212. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
  213. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
  214. claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
  215. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
  216. claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
  217. claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
  218. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
  219. claude_mpm/services/agents/deployment/results/__init__.py +13 -0
  220. claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
  221. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
  222. claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
  223. claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
  224. claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
  225. claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
  226. claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
  227. claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
  228. claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
  229. claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
  230. claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
  231. claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
  232. claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
  233. claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
  234. claude_mpm/services/agents/loading/__init__.py +2 -2
  235. claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
  236. claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
  237. claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
  238. claude_mpm/services/agents/management/__init__.py +2 -2
  239. claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
  240. claude_mpm/services/agents/management/agent_management_service.py +209 -156
  241. claude_mpm/services/agents/memory/__init__.py +9 -6
  242. claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
  243. claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
  244. claude_mpm/services/agents/memory/analyzer.py +430 -0
  245. claude_mpm/services/agents/memory/content_manager.py +376 -0
  246. claude_mpm/services/agents/memory/template_generator.py +468 -0
  247. claude_mpm/services/agents/registry/__init__.py +7 -10
  248. claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
  249. claude_mpm/services/agents/registry/modification_tracker.py +351 -285
  250. claude_mpm/services/async_session_logger.py +187 -153
  251. claude_mpm/services/claude_session_logger.py +87 -72
  252. claude_mpm/services/command_handler_service.py +217 -0
  253. claude_mpm/services/communication/__init__.py +3 -2
  254. claude_mpm/services/core/__init__.py +50 -97
  255. claude_mpm/services/core/base.py +60 -53
  256. claude_mpm/services/core/interfaces/__init__.py +188 -0
  257. claude_mpm/services/core/interfaces/agent.py +351 -0
  258. claude_mpm/services/core/interfaces/communication.py +343 -0
  259. claude_mpm/services/core/interfaces/infrastructure.py +413 -0
  260. claude_mpm/services/core/interfaces/service.py +434 -0
  261. claude_mpm/services/core/interfaces.py +19 -944
  262. claude_mpm/services/event_aggregator.py +208 -170
  263. claude_mpm/services/exceptions.py +387 -308
  264. claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
  265. claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
  266. claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
  267. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
  268. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
  269. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
  270. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
  271. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
  272. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
  273. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
  274. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
  275. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
  276. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
  277. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
  278. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
  279. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
  280. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
  281. claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
  283. claude_mpm/services/hook_service.py +106 -114
  284. claude_mpm/services/infrastructure/__init__.py +7 -5
  285. claude_mpm/services/infrastructure/context_preservation.py +571 -0
  286. claude_mpm/services/infrastructure/daemon_manager.py +279 -0
  287. claude_mpm/services/infrastructure/logging.py +83 -76
  288. claude_mpm/services/infrastructure/monitoring.py +547 -404
  289. claude_mpm/services/mcp_gateway/__init__.py +40 -23
  290. claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
  291. claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
  292. claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
  293. claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
  294. claude_mpm/services/mcp_gateway/core/__init__.py +14 -21
  295. claude_mpm/services/mcp_gateway/core/base.py +80 -67
  296. claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
  297. claude_mpm/services/mcp_gateway/core/interfaces.py +97 -93
  298. claude_mpm/services/mcp_gateway/main.py +307 -127
  299. claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +100 -101
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
  302. claude_mpm/services/mcp_gateway/server/__init__.py +4 -4
  303. claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +149 -153
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +110 -121
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
  309. claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
  310. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
  311. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
  312. claude_mpm/services/memory/__init__.py +2 -2
  313. claude_mpm/services/memory/builder.py +451 -362
  314. claude_mpm/services/memory/cache/__init__.py +2 -2
  315. claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
  316. claude_mpm/services/memory/cache/simple_cache.py +107 -93
  317. claude_mpm/services/memory/indexed_memory.py +195 -193
  318. claude_mpm/services/memory/optimizer.py +267 -234
  319. claude_mpm/services/memory/router.py +571 -263
  320. claude_mpm/services/memory_hook_service.py +237 -0
  321. claude_mpm/services/port_manager.py +223 -0
  322. claude_mpm/services/project/__init__.py +3 -3
  323. claude_mpm/services/project/analyzer.py +451 -305
  324. claude_mpm/services/project/registry.py +262 -240
  325. claude_mpm/services/recovery_manager.py +287 -231
  326. claude_mpm/services/response_tracker.py +87 -67
  327. claude_mpm/services/runner_configuration_service.py +587 -0
  328. claude_mpm/services/session_management_service.py +304 -0
  329. claude_mpm/services/socketio/__init__.py +4 -4
  330. claude_mpm/services/socketio/client_proxy.py +174 -0
  331. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  332. claude_mpm/services/socketio/handlers/base.py +44 -30
  333. claude_mpm/services/socketio/handlers/connection.py +145 -65
  334. claude_mpm/services/socketio/handlers/file.py +123 -108
  335. claude_mpm/services/socketio/handlers/git.py +607 -373
  336. claude_mpm/services/socketio/handlers/hook.py +170 -0
  337. claude_mpm/services/socketio/handlers/memory.py +4 -4
  338. claude_mpm/services/socketio/handlers/project.py +4 -4
  339. claude_mpm/services/socketio/handlers/registry.py +53 -38
  340. claude_mpm/services/socketio/server/__init__.py +18 -0
  341. claude_mpm/services/socketio/server/broadcaster.py +252 -0
  342. claude_mpm/services/socketio/server/core.py +399 -0
  343. claude_mpm/services/socketio/server/main.py +323 -0
  344. claude_mpm/services/socketio_client_manager.py +160 -133
  345. claude_mpm/services/socketio_server.py +36 -1885
  346. claude_mpm/services/subprocess_launcher_service.py +316 -0
  347. claude_mpm/services/system_instructions_service.py +258 -0
  348. claude_mpm/services/ticket_manager.py +20 -534
  349. claude_mpm/services/utility_service.py +285 -0
  350. claude_mpm/services/version_control/__init__.py +18 -21
  351. claude_mpm/services/version_control/branch_strategy.py +20 -10
  352. claude_mpm/services/version_control/conflict_resolution.py +37 -13
  353. claude_mpm/services/version_control/git_operations.py +52 -21
  354. claude_mpm/services/version_control/semantic_versioning.py +92 -53
  355. claude_mpm/services/version_control/version_parser.py +145 -125
  356. claude_mpm/services/version_service.py +270 -0
  357. claude_mpm/storage/__init__.py +9 -0
  358. claude_mpm/storage/state_storage.py +552 -0
  359. claude_mpm/ticket_wrapper.py +2 -2
  360. claude_mpm/utils/__init__.py +2 -2
  361. claude_mpm/utils/agent_dependency_loader.py +453 -243
  362. claude_mpm/utils/config_manager.py +157 -118
  363. claude_mpm/utils/console.py +1 -1
  364. claude_mpm/utils/dependency_cache.py +102 -107
  365. claude_mpm/utils/dependency_manager.py +52 -47
  366. claude_mpm/utils/dependency_strategies.py +131 -96
  367. claude_mpm/utils/environment_context.py +110 -102
  368. claude_mpm/utils/error_handler.py +75 -55
  369. claude_mpm/utils/file_utils.py +80 -67
  370. claude_mpm/utils/framework_detection.py +12 -11
  371. claude_mpm/utils/import_migration_example.py +12 -60
  372. claude_mpm/utils/imports.py +48 -45
  373. claude_mpm/utils/path_operations.py +100 -93
  374. claude_mpm/utils/robust_installer.py +172 -164
  375. claude_mpm/utils/session_logging.py +30 -23
  376. claude_mpm/utils/subprocess_utils.py +99 -61
  377. claude_mpm/validation/__init__.py +1 -1
  378. claude_mpm/validation/agent_validator.py +151 -111
  379. claude_mpm/validation/frontmatter_validator.py +92 -71
  380. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/METADATA +51 -2
  381. claude_mpm-4.0.3.dist-info/RECORD +402 -0
  382. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/entry_points.txt +1 -0
  383. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/licenses/LICENSE +1 -1
  384. claude_mpm/config/memory_guardian_config.py +0 -325
  385. claude_mpm/core/config_paths.py +0 -150
  386. claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
  387. claude_mpm/deployment_paths.py +0 -261
  388. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
  389. claude_mpm/models/state_models.py +0 -433
  390. claude_mpm/services/agent/__init__.py +0 -24
  391. claude_mpm/services/agent/deployment.py +0 -2548
  392. claude_mpm/services/agent/management.py +0 -598
  393. claude_mpm/services/agent/registry.py +0 -813
  394. claude_mpm/services/agents/registry/agent_registry.py +0 -813
  395. claude_mpm/services/communication/socketio.py +0 -1935
  396. claude_mpm/services/communication/websocket.py +0 -479
  397. claude_mpm/services/framework_claude_md_generator.py +0 -624
  398. claude_mpm/services/health_monitor.py +0 -893
  399. claude_mpm/services/infrastructure/memory_guardian.py +0 -770
  400. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
  401. claude_mpm/services/optimized_hook_service.py +0 -542
  402. claude_mpm/services/project_analyzer.py +0 -864
  403. claude_mpm/services/project_registry.py +0 -608
  404. claude_mpm/services/standalone_socketio_server.py +0 -1300
  405. claude_mpm/services/ticket_manager_di.py +0 -318
  406. claude_mpm/services/ticketing_service_original.py +0 -510
  407. claude_mpm/utils/paths.py +0 -395
  408. claude_mpm/utils/platform_memory.py +0 -524
  409. claude_mpm-3.9.9.dist-info/RECORD +0 -293
  410. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/WHEEL +0 -0
  411. {claude_mpm-3.9.9.dist-info → claude_mpm-4.0.3.dist-info}/top_level.txt +0 -0
@@ -5,561 +5,743 @@ file tracking status, and git add operations. Isolating git operations
5
5
  improves maintainability and makes it easier to extend git functionality.
6
6
  """
7
7
 
8
+ import asyncio
8
9
  import os
9
10
  import subprocess
10
- import asyncio
11
- from typing import Optional, Dict, Any, List
12
11
  from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
13
14
 
15
+ from ....core.typing_utils import EventData, PathLike, SocketId
14
16
  from .base import BaseEventHandler
15
- from ....core.typing_utils import SocketId, EventData, PathLike
16
17
 
17
18
 
18
19
  class GitEventHandler(BaseEventHandler):
19
20
  """Handles git-related Socket.IO events.
20
-
21
+
21
22
  WHY: Git operations are a distinct domain that benefits from focused
22
23
  handling. This includes checking branches, file tracking status,
23
24
  and adding files to git. Separating these improves code organization.
24
25
  """
25
-
26
+
26
27
  def register_events(self) -> None:
27
28
  """Register git-related event handlers."""
28
-
29
+
29
30
  @self.sio.event
30
31
  async def get_git_branch(sid, working_dir=None):
31
32
  """Get the current git branch for a directory.
32
-
33
+
33
34
  WHY: The dashboard needs to display the current git branch
34
35
  to provide context about which branch changes are being made on.
35
36
  """
36
37
  try:
37
- self.logger.info(f"[GIT-BRANCH-DEBUG] get_git_branch called with working_dir: {repr(working_dir)} (type: {type(working_dir)})")
38
-
38
+ # Debug: get_git_branch called
39
+
39
40
  # Validate and sanitize working directory
40
41
  working_dir = self._sanitize_working_dir(working_dir, "get_git_branch")
41
-
42
- if not self._validate_directory(sid, working_dir, "git_branch_response"):
42
+
43
+ if not await self._validate_directory(
44
+ sid, working_dir, "git_branch_response"
45
+ ):
43
46
  return
44
-
45
- self.logger.info(f"[GIT-BRANCH-DEBUG] Running git command in directory: {working_dir}")
46
-
47
+
48
+ # Debug: Running git command
49
+
47
50
  # Run git command to get current branch
48
51
  result = subprocess.run(
49
52
  ["git", "rev-parse", "--abbrev-ref", "HEAD"],
50
53
  cwd=working_dir,
51
54
  capture_output=True,
52
- text=True
55
+ text=True,
53
56
  )
54
-
55
- self.logger.info(f"[GIT-BRANCH-DEBUG] Git command result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
56
-
57
+
58
+ # Debug: Git command completed
59
+
57
60
  if result.returncode == 0:
58
61
  branch = result.stdout.strip()
59
- self.logger.info(f"[GIT-BRANCH-DEBUG] Successfully got git branch: {branch}")
60
- await self.emit_to_client(sid, 'git_branch_response', {
61
- 'success': True,
62
- 'branch': branch,
63
- 'working_dir': working_dir,
64
- 'original_working_dir': working_dir
65
- })
62
+ # Debug: Successfully got git branch
63
+ await self.emit_to_client(
64
+ sid,
65
+ "git_branch_response",
66
+ {
67
+ "success": True,
68
+ "branch": branch,
69
+ "working_dir": working_dir,
70
+ "original_working_dir": working_dir,
71
+ },
72
+ )
66
73
  else:
67
- self.logger.warning(f"[GIT-BRANCH-DEBUG] Git command failed: {result.stderr}")
68
- await self.emit_to_client(sid, 'git_branch_response', {
69
- 'success': False,
70
- 'error': 'Not a git repository',
71
- 'working_dir': working_dir,
72
- 'original_working_dir': working_dir,
73
- 'git_error': result.stderr
74
- })
75
-
74
+ self.logger.warning(f"Git command failed: {result.stderr}")
75
+ await self.emit_to_client(
76
+ sid,
77
+ "git_branch_response",
78
+ {
79
+ "success": False,
80
+ "error": "Not a git repository",
81
+ "working_dir": working_dir,
82
+ "original_working_dir": working_dir,
83
+ "git_error": result.stderr,
84
+ },
85
+ )
86
+
76
87
  except Exception as e:
77
88
  self.log_error("get_git_branch", e, {"working_dir": working_dir})
78
- await self.emit_to_client(sid, 'git_branch_response', {
79
- 'success': False,
80
- 'error': str(e),
81
- 'working_dir': working_dir,
82
- 'original_working_dir': working_dir
83
- })
84
-
89
+ await self.emit_to_client(
90
+ sid,
91
+ "git_branch_response",
92
+ {
93
+ "success": False,
94
+ "error": str(e),
95
+ "working_dir": working_dir,
96
+ "original_working_dir": working_dir,
97
+ },
98
+ )
99
+
85
100
  @self.sio.event
86
101
  async def check_file_tracked(sid, data):
87
102
  """Check if a file is tracked by git.
88
-
103
+
89
104
  WHY: The dashboard needs to know if a file is tracked by git
90
105
  to determine whether to show git-related UI elements.
91
106
  """
92
107
  try:
93
- file_path = data.get('file_path')
94
- working_dir = data.get('working_dir', os.getcwd())
95
-
108
+ file_path = data.get("file_path")
109
+ working_dir = data.get("working_dir", os.getcwd())
110
+
96
111
  if not file_path:
97
- await self.emit_to_client(sid, 'file_tracked_response', {
98
- 'success': False,
99
- 'error': 'file_path is required',
100
- 'file_path': file_path
101
- })
112
+ await self.emit_to_client(
113
+ sid,
114
+ "file_tracked_response",
115
+ {
116
+ "success": False,
117
+ "error": "file_path is required",
118
+ "file_path": file_path,
119
+ },
120
+ )
102
121
  return
103
-
122
+
104
123
  # Use git ls-files to check if file is tracked
105
124
  result = subprocess.run(
106
125
  ["git", "-C", working_dir, "ls-files", "--", file_path],
107
126
  capture_output=True,
108
- text=True
127
+ text=True,
109
128
  )
110
-
129
+
111
130
  is_tracked = result.returncode == 0 and result.stdout.strip()
112
-
113
- await self.emit_to_client(sid, 'file_tracked_response', {
114
- 'success': True,
115
- 'file_path': file_path,
116
- 'working_dir': working_dir,
117
- 'is_tracked': bool(is_tracked)
118
- })
119
-
131
+
132
+ await self.emit_to_client(
133
+ sid,
134
+ "file_tracked_response",
135
+ {
136
+ "success": True,
137
+ "file_path": file_path,
138
+ "working_dir": working_dir,
139
+ "is_tracked": bool(is_tracked),
140
+ },
141
+ )
142
+
120
143
  except Exception as e:
121
144
  self.log_error("check_file_tracked", e, data)
122
- await self.emit_to_client(sid, 'file_tracked_response', {
123
- 'success': False,
124
- 'error': str(e),
125
- 'file_path': data.get('file_path', 'unknown')
126
- })
127
-
145
+ await self.emit_to_client(
146
+ sid,
147
+ "file_tracked_response",
148
+ {
149
+ "success": False,
150
+ "error": str(e),
151
+ "file_path": data.get("file_path", "unknown"),
152
+ },
153
+ )
154
+
128
155
  @self.sio.event
129
156
  async def check_git_status(sid, data):
130
157
  """Check git status for a file to determine if git diff icons should be shown.
131
-
158
+
132
159
  WHY: The dashboard shows git diff icons for files that have changes.
133
160
  This checks if a file has git status to determine icon visibility.
134
161
  """
135
162
  try:
136
- file_path = data.get('file_path')
137
- working_dir = data.get('working_dir', os.getcwd())
138
-
139
- self.logger.info(f"[GIT-STATUS-DEBUG] check_git_status called with file_path: {repr(file_path)}, working_dir: {repr(working_dir)}")
140
-
163
+ file_path = data.get("file_path")
164
+ working_dir = data.get("working_dir", os.getcwd())
165
+
166
+ # Debug: check_git_status called
167
+
141
168
  if not file_path:
142
- await self.emit_to_client(sid, 'git_status_response', {
143
- 'success': False,
144
- 'error': 'file_path is required',
145
- 'file_path': file_path
146
- })
169
+ await self.emit_to_client(
170
+ sid,
171
+ "git_status_response",
172
+ {
173
+ "success": False,
174
+ "error": "file_path is required",
175
+ "file_path": file_path,
176
+ },
177
+ )
147
178
  return
148
-
179
+
149
180
  # Validate and sanitize working_dir
150
181
  original_working_dir = working_dir
151
- working_dir = self._sanitize_working_dir(working_dir, "check_git_status")
152
-
153
- if not self._validate_directory_for_status(sid, working_dir, original_working_dir, file_path):
182
+ working_dir = self._sanitize_working_dir(
183
+ working_dir, "check_git_status"
184
+ )
185
+
186
+ if not await self._validate_directory_for_status(
187
+ sid, working_dir, original_working_dir, file_path
188
+ ):
154
189
  return
155
-
190
+
156
191
  # Check if this is a git repository
157
192
  if not self._is_git_repository(working_dir):
158
- await self.emit_to_client(sid, 'git_status_response', {
159
- 'success': False,
160
- 'error': 'Not a git repository',
161
- 'file_path': file_path,
162
- 'working_dir': working_dir,
163
- 'original_working_dir': original_working_dir
164
- })
193
+ await self.emit_to_client(
194
+ sid,
195
+ "git_status_response",
196
+ {
197
+ "success": False,
198
+ "error": "Not a git repository",
199
+ "file_path": file_path,
200
+ "working_dir": working_dir,
201
+ "original_working_dir": original_working_dir,
202
+ },
203
+ )
165
204
  return
166
-
205
+
167
206
  # Check git status for the file
168
- file_path_for_git = self._make_path_relative_to_git(file_path, working_dir)
169
-
207
+ file_path_for_git = self._make_path_relative_to_git(
208
+ file_path, working_dir
209
+ )
210
+
170
211
  # Check if the file exists
171
- full_path = file_path if os.path.isabs(file_path) else os.path.join(working_dir, file_path)
172
- if not os.path.exists(full_path):
173
- self.logger.warning(f"[GIT-STATUS-DEBUG] File does not exist: {full_path}")
174
- await self.emit_to_client(sid, 'git_status_response', {
175
- 'success': False,
176
- 'error': f'File does not exist: {file_path}',
177
- 'file_path': file_path,
178
- 'working_dir': working_dir,
179
- 'original_working_dir': original_working_dir
180
- })
212
+ file_path_obj = Path(file_path)
213
+ full_path = (
214
+ file_path_obj
215
+ if file_path_obj.is_absolute()
216
+ else Path(working_dir) / file_path
217
+ )
218
+ if not full_path.exists():
219
+ self.logger.warning(f"File does not exist: {full_path}")
220
+ await self.emit_to_client(
221
+ sid,
222
+ "git_status_response",
223
+ {
224
+ "success": False,
225
+ "error": f"File does not exist: {file_path}",
226
+ "file_path": file_path,
227
+ "working_dir": working_dir,
228
+ "original_working_dir": original_working_dir,
229
+ },
230
+ )
181
231
  return
182
-
232
+
183
233
  # Check git status and tracking
184
- is_tracked, has_changes = self._check_file_git_status(file_path_for_git, working_dir)
185
-
234
+ is_tracked, has_changes = self._check_file_git_status(
235
+ file_path_for_git, working_dir
236
+ )
237
+
186
238
  if is_tracked or has_changes:
187
- self.logger.info(f"[GIT-STATUS-DEBUG] Git status check successful for {file_path}")
188
- await self.emit_to_client(sid, 'git_status_response', {
189
- 'success': True,
190
- 'file_path': file_path,
191
- 'working_dir': working_dir,
192
- 'original_working_dir': original_working_dir,
193
- 'is_tracked': is_tracked,
194
- 'has_changes': has_changes
195
- })
239
+ # Debug: Git status check successful
240
+ await self.emit_to_client(
241
+ sid,
242
+ "git_status_response",
243
+ {
244
+ "success": True,
245
+ "file_path": file_path,
246
+ "working_dir": working_dir,
247
+ "original_working_dir": original_working_dir,
248
+ "is_tracked": is_tracked,
249
+ "has_changes": has_changes,
250
+ },
251
+ )
196
252
  else:
197
- self.logger.info(f"[GIT-STATUS-DEBUG] File {file_path} is not tracked by git")
198
- await self.emit_to_client(sid, 'git_status_response', {
199
- 'success': False,
200
- 'error': 'File is not tracked by git',
201
- 'file_path': file_path,
202
- 'working_dir': working_dir,
203
- 'original_working_dir': original_working_dir,
204
- 'is_tracked': False
205
- })
206
-
253
+ # Debug: File is not tracked by git
254
+ await self.emit_to_client(
255
+ sid,
256
+ "git_status_response",
257
+ {
258
+ "success": False,
259
+ "error": "File is not tracked by git",
260
+ "file_path": file_path,
261
+ "working_dir": working_dir,
262
+ "original_working_dir": original_working_dir,
263
+ "is_tracked": False,
264
+ },
265
+ )
266
+
207
267
  except Exception as e:
208
268
  self.log_error("check_git_status", e, data)
209
- await self.emit_to_client(sid, 'git_status_response', {
210
- 'success': False,
211
- 'error': str(e),
212
- 'file_path': data.get('file_path', 'unknown'),
213
- 'working_dir': data.get('working_dir', 'unknown')
214
- })
215
-
269
+ await self.emit_to_client(
270
+ sid,
271
+ "git_status_response",
272
+ {
273
+ "success": False,
274
+ "error": str(e),
275
+ "file_path": data.get("file_path", "unknown"),
276
+ "working_dir": data.get("working_dir", "unknown"),
277
+ },
278
+ )
279
+
216
280
  @self.sio.event
217
281
  async def git_add_file(sid, data):
218
282
  """Add file to git tracking.
219
-
283
+
220
284
  WHY: Users can add untracked files to git directly from the dashboard,
221
285
  making it easier to manage version control without leaving the UI.
222
286
  """
223
287
  try:
224
- file_path = data.get('file_path')
225
- working_dir = data.get('working_dir', os.getcwd())
226
-
227
- 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)})")
228
-
288
+ file_path = data.get("file_path")
289
+ working_dir = data.get("working_dir", os.getcwd())
290
+
291
+ # Debug: git_add_file called
292
+
229
293
  if not file_path:
230
- await self.emit_to_client(sid, 'git_add_response', {
231
- 'success': False,
232
- 'error': 'file_path is required',
233
- 'file_path': file_path
234
- })
294
+ await self.emit_to_client(
295
+ sid,
296
+ "git_add_response",
297
+ {
298
+ "success": False,
299
+ "error": "file_path is required",
300
+ "file_path": file_path,
301
+ },
302
+ )
235
303
  return
236
-
304
+
237
305
  # Validate and sanitize working_dir
238
306
  original_working_dir = working_dir
239
307
  working_dir = self._sanitize_working_dir(working_dir, "git_add_file")
240
-
241
- if not self._validate_directory_for_add(sid, working_dir, original_working_dir, file_path):
308
+
309
+ if not await self._validate_directory_for_add(
310
+ sid, working_dir, original_working_dir, file_path
311
+ ):
242
312
  return
243
-
244
- self.logger.info(f"[GIT-ADD-DEBUG] Running git add command in directory: {working_dir}")
245
-
313
+
314
+ # Debug: Running git add command
315
+
246
316
  # Use git add to track the file
247
317
  result = subprocess.run(
248
318
  ["git", "-C", working_dir, "add", file_path],
249
319
  capture_output=True,
250
- text=True
320
+ text=True,
251
321
  )
252
-
253
- self.logger.info(f"[GIT-ADD-DEBUG] Git add result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
254
-
322
+
323
+ # Debug: Git add completed
324
+
255
325
  if result.returncode == 0:
256
- self.logger.info(f"[GIT-ADD-DEBUG] Successfully added {file_path} to git in {working_dir}")
257
- await self.emit_to_client(sid, 'git_add_response', {
258
- 'success': True,
259
- 'file_path': file_path,
260
- 'working_dir': working_dir,
261
- 'original_working_dir': original_working_dir,
262
- 'message': 'File successfully added to git tracking'
263
- })
326
+ # Debug: Successfully added file to git
327
+ await self.emit_to_client(
328
+ sid,
329
+ "git_add_response",
330
+ {
331
+ "success": True,
332
+ "file_path": file_path,
333
+ "working_dir": working_dir,
334
+ "original_working_dir": original_working_dir,
335
+ "message": "File successfully added to git tracking",
336
+ },
337
+ )
264
338
  else:
265
- error_message = result.stderr.strip() or 'Unknown git error'
266
- self.logger.warning(f"[GIT-ADD-DEBUG] Git add failed: {error_message}")
267
- await self.emit_to_client(sid, 'git_add_response', {
268
- 'success': False,
269
- 'error': f'Git add failed: {error_message}',
270
- 'file_path': file_path,
271
- 'working_dir': working_dir,
272
- 'original_working_dir': original_working_dir
273
- })
274
-
339
+ error_message = result.stderr.strip() or "Unknown git error"
340
+ self.logger.warning(f"Git add failed: {error_message}")
341
+ await self.emit_to_client(
342
+ sid,
343
+ "git_add_response",
344
+ {
345
+ "success": False,
346
+ "error": f"Git add failed: {error_message}",
347
+ "file_path": file_path,
348
+ "working_dir": working_dir,
349
+ "original_working_dir": original_working_dir,
350
+ },
351
+ )
352
+
275
353
  except Exception as e:
276
354
  self.log_error("git_add_file", e, data)
277
- await self.emit_to_client(sid, 'git_add_response', {
278
- 'success': False,
279
- 'error': str(e),
280
- 'file_path': data.get('file_path', 'unknown'),
281
- 'working_dir': data.get('working_dir', 'unknown')
282
- })
283
-
355
+ await self.emit_to_client(
356
+ sid,
357
+ "git_add_response",
358
+ {
359
+ "success": False,
360
+ "error": str(e),
361
+ "file_path": data.get("file_path", "unknown"),
362
+ "working_dir": data.get("working_dir", "unknown"),
363
+ },
364
+ )
365
+
284
366
  def _sanitize_working_dir(self, working_dir: Optional[str], operation: str) -> str:
285
367
  """Sanitize and validate working directory input.
286
-
368
+
287
369
  WHY: Working directory input from clients can be invalid or malformed.
288
370
  This ensures we have a valid directory path to work with.
289
371
  """
290
372
  invalid_states = [
291
- None, '', 'Unknown', 'Loading...', 'Loading', 'undefined', 'null',
292
- 'Not Connected', 'Invalid Directory', 'No Directory', '.'
373
+ None,
374
+ "",
375
+ "Unknown",
376
+ "Loading...",
377
+ "Loading",
378
+ "undefined",
379
+ "null",
380
+ "Not Connected",
381
+ "Invalid Directory",
382
+ "No Directory",
383
+ ".",
293
384
  ]
294
-
385
+
295
386
  original_working_dir = working_dir
296
- if working_dir in invalid_states or (isinstance(working_dir, str) and working_dir.strip() == ''):
387
+ if working_dir in invalid_states or (
388
+ isinstance(working_dir, str) and working_dir.strip() == ""
389
+ ):
297
390
  working_dir = os.getcwd()
298
- self.logger.info(f"[{operation}] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
391
+ self.logger.info(
392
+ f"[{operation}] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}"
393
+ )
299
394
  else:
300
395
  self.logger.info(f"[{operation}] Using provided working_dir: {working_dir}")
301
-
396
+
302
397
  # Additional validation for obviously invalid paths
303
398
  if isinstance(working_dir, str):
304
399
  working_dir = working_dir.strip()
305
400
  # Check for null bytes or other invalid characters
306
- if '\x00' in working_dir:
307
- self.logger.warning(f"[{operation}] working_dir contains null bytes, using cwd instead")
401
+ if "\x00" in working_dir:
402
+ self.logger.warning(
403
+ f"[{operation}] working_dir contains null bytes, using cwd instead"
404
+ )
308
405
  working_dir = os.getcwd()
309
-
406
+
310
407
  return working_dir
311
-
312
- async def _validate_directory(self, sid: str, working_dir: str, response_event: str) -> bool:
408
+
409
+ async def _validate_directory(
410
+ self, sid: str, working_dir: str, response_event: str
411
+ ) -> bool:
313
412
  """Validate that a directory exists and is accessible.
314
-
413
+
315
414
  WHY: We need to ensure the directory exists and is a directory
316
415
  before attempting git operations on it.
317
416
  """
318
- if not os.path.exists(working_dir):
319
- self.logger.info(f"Directory does not exist: {working_dir} - responding gracefully")
320
- await self.emit_to_client(sid, response_event, {
321
- 'success': False,
322
- 'error': f'Directory not found',
323
- 'working_dir': working_dir,
324
- 'detail': f'Path does not exist: {working_dir}'
325
- })
417
+ working_dir_path = Path(working_dir)
418
+ if not working_dir_path.exists():
419
+ self.logger.info(
420
+ f"Directory does not exist: {working_dir} - responding gracefully"
421
+ )
422
+ await self.emit_to_client(
423
+ sid,
424
+ response_event,
425
+ {
426
+ "success": False,
427
+ "error": f"Directory not found",
428
+ "working_dir": working_dir,
429
+ "detail": f"Path does not exist: {working_dir}",
430
+ },
431
+ )
326
432
  return False
327
-
328
- if not os.path.isdir(working_dir):
329
- self.logger.info(f"Path is not a directory: {working_dir} - responding gracefully")
330
- await self.emit_to_client(sid, response_event, {
331
- 'success': False,
332
- 'error': f'Not a directory',
333
- 'working_dir': working_dir,
334
- 'detail': f'Path is not a directory: {working_dir}'
335
- })
433
+
434
+ if not working_dir_path.is_dir():
435
+ self.logger.info(
436
+ f"Path is not a directory: {working_dir} - responding gracefully"
437
+ )
438
+ await self.emit_to_client(
439
+ sid,
440
+ response_event,
441
+ {
442
+ "success": False,
443
+ "error": f"Not a directory",
444
+ "working_dir": working_dir,
445
+ "detail": f"Path is not a directory: {working_dir}",
446
+ },
447
+ )
336
448
  return False
337
-
449
+
338
450
  return True
339
-
340
- async def _validate_directory_for_status(self, sid: str, working_dir: str, original_working_dir: str, file_path: str) -> bool:
451
+
452
+ async def _validate_directory_for_status(
453
+ self, sid: str, working_dir: str, original_working_dir: str, file_path: str
454
+ ) -> bool:
341
455
  """Validate directory for git status operations."""
342
- if not os.path.exists(working_dir):
343
- self.logger.warning(f"[GIT-STATUS-DEBUG] Directory does not exist: {working_dir}")
344
- await self.emit_to_client(sid, 'git_status_response', {
345
- 'success': False,
346
- 'error': f'Directory does not exist: {working_dir}',
347
- 'file_path': file_path,
348
- 'working_dir': working_dir,
349
- 'original_working_dir': original_working_dir
350
- })
456
+ working_dir_path = Path(working_dir)
457
+ if not working_dir_path.exists():
458
+ self.logger.warning(f"Directory does not exist: {working_dir}")
459
+ await self.emit_to_client(
460
+ sid,
461
+ "git_status_response",
462
+ {
463
+ "success": False,
464
+ "error": f"Directory does not exist: {working_dir}",
465
+ "file_path": file_path,
466
+ "working_dir": working_dir,
467
+ "original_working_dir": original_working_dir,
468
+ },
469
+ )
351
470
  return False
352
-
353
- if not os.path.isdir(working_dir):
354
- self.logger.warning(f"[GIT-STATUS-DEBUG] Path is not a directory: {working_dir}")
355
- await self.emit_to_client(sid, 'git_status_response', {
356
- 'success': False,
357
- 'error': f'Path is not a directory: {working_dir}',
358
- 'file_path': file_path,
359
- 'working_dir': working_dir,
360
- 'original_working_dir': original_working_dir
361
- })
471
+
472
+ if not working_dir_path.is_dir():
473
+ self.logger.warning(f"Path is not a directory: {working_dir}")
474
+ await self.emit_to_client(
475
+ sid,
476
+ "git_status_response",
477
+ {
478
+ "success": False,
479
+ "error": f"Path is not a directory: {working_dir}",
480
+ "file_path": file_path,
481
+ "working_dir": working_dir,
482
+ "original_working_dir": original_working_dir,
483
+ },
484
+ )
362
485
  return False
363
-
486
+
364
487
  return True
365
-
366
- async def _validate_directory_for_add(self, sid: str, working_dir: str, original_working_dir: str, file_path: str) -> bool:
488
+
489
+ async def _validate_directory_for_add(
490
+ self, sid: str, working_dir: str, original_working_dir: str, file_path: str
491
+ ) -> bool:
367
492
  """Validate directory for git add operations."""
368
- if not os.path.exists(working_dir):
369
- self.logger.warning(f"[GIT-ADD-DEBUG] Directory does not exist: {working_dir}")
370
- await self.emit_to_client(sid, 'git_add_response', {
371
- 'success': False,
372
- 'error': f'Directory does not exist: {working_dir}',
373
- 'file_path': file_path,
374
- 'working_dir': working_dir,
375
- 'original_working_dir': original_working_dir
376
- })
493
+ working_dir_path = Path(working_dir)
494
+ if not working_dir_path.exists():
495
+ self.logger.warning(f"Directory does not exist: {working_dir}")
496
+ await self.emit_to_client(
497
+ sid,
498
+ "git_add_response",
499
+ {
500
+ "success": False,
501
+ "error": f"Directory does not exist: {working_dir}",
502
+ "file_path": file_path,
503
+ "working_dir": working_dir,
504
+ "original_working_dir": original_working_dir,
505
+ },
506
+ )
377
507
  return False
378
-
379
- if not os.path.isdir(working_dir):
380
- self.logger.warning(f"[GIT-ADD-DEBUG] Path is not a directory: {working_dir}")
381
- await self.emit_to_client(sid, 'git_add_response', {
382
- 'success': False,
383
- 'error': f'Path is not a directory: {working_dir}',
384
- 'file_path': file_path,
385
- 'working_dir': working_dir,
386
- 'original_working_dir': original_working_dir
387
- })
508
+
509
+ if not working_dir_path.is_dir():
510
+ self.logger.warning(f"Path is not a directory: {working_dir}")
511
+ await self.emit_to_client(
512
+ sid,
513
+ "git_add_response",
514
+ {
515
+ "success": False,
516
+ "error": f"Path is not a directory: {working_dir}",
517
+ "file_path": file_path,
518
+ "working_dir": working_dir,
519
+ "original_working_dir": original_working_dir,
520
+ },
521
+ )
388
522
  return False
389
-
523
+
390
524
  return True
391
-
525
+
392
526
  def _is_git_repository(self, working_dir: str) -> bool:
393
527
  """Check if a directory is a git repository."""
394
528
  git_check = subprocess.run(
395
529
  ["git", "-C", working_dir, "rev-parse", "--git-dir"],
396
530
  capture_output=True,
397
- text=True
531
+ text=True,
398
532
  )
399
533
  return git_check.returncode == 0
400
-
534
+
401
535
  def _make_path_relative_to_git(self, file_path: str, working_dir: str) -> str:
402
536
  """Make an absolute path relative to the git root if needed."""
403
- if not os.path.isabs(file_path):
537
+ file_path_obj = Path(file_path)
538
+ if not file_path_obj.is_absolute():
404
539
  return file_path
405
-
540
+
406
541
  # Get git root to make path relative if needed
407
542
  git_root_result = subprocess.run(
408
543
  ["git", "-C", working_dir, "rev-parse", "--show-toplevel"],
409
544
  capture_output=True,
410
- text=True
545
+ text=True,
411
546
  )
412
-
547
+
413
548
  if git_root_result.returncode == 0:
414
- git_root = git_root_result.stdout.strip()
549
+ git_root = Path(git_root_result.stdout.strip())
415
550
  try:
416
- relative_path = os.path.relpath(file_path, git_root)
417
- self.logger.info(f"Made file path relative to git root: {relative_path}")
418
- return relative_path
551
+ relative_path = file_path_obj.relative_to(git_root)
552
+ self.logger.info(
553
+ f"Made file path relative to git root: {relative_path}"
554
+ )
555
+ return str(relative_path)
419
556
  except ValueError:
420
557
  # File is not under git root - keep original path
421
- self.logger.info(f"File not under git root, keeping original path: {file_path}")
422
-
558
+ self.logger.info(
559
+ f"File not under git root, keeping original path: {file_path}"
560
+ )
561
+
423
562
  return file_path
424
-
425
- def _check_file_git_status(self, file_path: str, working_dir: str) -> tuple[bool, bool]:
563
+
564
+ def _check_file_git_status(
565
+ self, file_path: str, working_dir: str
566
+ ) -> tuple[bool, bool]:
426
567
  """Check if a file is tracked and has changes."""
427
568
  # Check git status for the file
428
569
  git_status_result = subprocess.run(
429
570
  ["git", "-C", working_dir, "status", "--porcelain", file_path],
430
571
  capture_output=True,
431
- text=True
572
+ text=True,
432
573
  )
433
-
574
+
434
575
  # Check if file is tracked by git
435
576
  ls_files_result = subprocess.run(
436
577
  ["git", "-C", working_dir, "ls-files", file_path],
437
578
  capture_output=True,
438
- text=True
579
+ text=True,
439
580
  )
440
-
581
+
441
582
  is_tracked = ls_files_result.returncode == 0 and ls_files_result.stdout.strip()
442
- has_changes = git_status_result.returncode == 0 and bool(git_status_result.stdout.strip())
443
-
444
- self.logger.info(f"File tracking status: is_tracked={is_tracked}, has_changes={has_changes}")
445
-
583
+ has_changes = git_status_result.returncode == 0 and bool(
584
+ git_status_result.stdout.strip()
585
+ )
586
+
587
+ self.logger.info(
588
+ f"File tracking status: is_tracked={is_tracked}, has_changes={has_changes}"
589
+ )
590
+
446
591
  return is_tracked, has_changes
447
-
448
- async def generate_git_diff(self, file_path: str, timestamp: Optional[str] = None, working_dir: Optional[str] = None) -> Dict[str, Any]:
592
+
593
+ async def generate_git_diff(
594
+ self,
595
+ file_path: str,
596
+ timestamp: Optional[str] = None,
597
+ working_dir: Optional[str] = None,
598
+ ) -> Dict[str, Any]:
449
599
  """Generate git diff for a specific file operation.
450
-
600
+
451
601
  WHY: This method generates a git diff showing the changes made to a file
452
602
  during a specific write operation. It uses git log and show commands to
453
603
  find the most relevant commit around the specified timestamp.
454
-
604
+
455
605
  Args:
456
606
  file_path: Path to the file relative to the git repository
457
607
  timestamp: ISO timestamp of the file operation (optional)
458
608
  working_dir: Working directory containing the git repository
459
-
609
+
460
610
  Returns:
461
611
  dict: Contains diff content, metadata, and status information
462
612
  """
463
613
  try:
464
614
  # If file_path is absolute, determine its git repository
465
- if os.path.isabs(file_path):
615
+ file_path_obj = Path(file_path)
616
+ if file_path_obj.is_absolute():
466
617
  # Find the directory containing the file
467
- file_dir = os.path.dirname(file_path)
468
- if os.path.exists(file_dir):
618
+ file_dir = file_path_obj.parent
619
+ if file_dir.exists():
469
620
  # Try to find the git root from the file's directory
470
621
  current_dir = file_dir
471
- while current_dir != "/" and current_dir:
472
- if os.path.exists(os.path.join(current_dir, ".git")):
473
- working_dir = current_dir
622
+ while current_dir != current_dir.parent: # Stop at root
623
+ if (current_dir / ".git").exists():
624
+ working_dir = str(current_dir)
474
625
  self.logger.info(f"Found git repository at: {working_dir}")
475
626
  break
476
- current_dir = os.path.dirname(current_dir)
627
+ current_dir = current_dir.parent
477
628
  else:
478
629
  # If no git repo found, use the file's directory
479
- working_dir = file_dir
480
- self.logger.info(f"No git repo found, using file's directory: {working_dir}")
481
-
630
+ working_dir = str(file_dir)
631
+ self.logger.info(
632
+ f"No git repo found, using file's directory: {working_dir}"
633
+ )
634
+
482
635
  # Handle case where working_dir is None, empty string, or 'Unknown'
483
636
  original_working_dir = working_dir
484
- if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '':
637
+ if not working_dir or working_dir == "Unknown" or working_dir.strip() == "":
485
638
  working_dir = os.getcwd()
486
- self.logger.info(f"[GIT-DIFF-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
639
+ # Debug: working_dir was invalid, using cwd
487
640
  else:
488
- self.logger.info(f"[GIT-DIFF-DEBUG] Using provided working_dir: {working_dir}")
489
-
641
+ # Debug: Using provided working_dir
642
+ pass
643
+
490
644
  # For read-only git operations, we can work from any directory
491
645
  # by passing the -C flag to git commands instead of changing directories
492
646
  original_cwd = os.getcwd()
493
647
  try:
494
648
  # We'll use git -C <working_dir> for all commands instead of chdir
495
-
649
+
496
650
  # Check if this is a git repository
497
651
  git_check = await asyncio.create_subprocess_exec(
498
- 'git', '-C', working_dir, 'rev-parse', '--git-dir',
652
+ "git",
653
+ "-C",
654
+ working_dir,
655
+ "rev-parse",
656
+ "--git-dir",
499
657
  stdout=asyncio.subprocess.PIPE,
500
- stderr=asyncio.subprocess.PIPE
658
+ stderr=asyncio.subprocess.PIPE,
501
659
  )
502
660
  await git_check.communicate()
503
-
661
+
504
662
  if git_check.returncode != 0:
505
663
  return {
506
664
  "success": False,
507
665
  "error": "Not a git repository",
508
666
  "file_path": file_path,
509
- "working_dir": working_dir
667
+ "working_dir": working_dir,
510
668
  }
511
-
669
+
512
670
  # Get the absolute path of the file relative to git root
513
671
  git_root_proc = await asyncio.create_subprocess_exec(
514
- 'git', '-C', working_dir, 'rev-parse', '--show-toplevel',
672
+ "git",
673
+ "-C",
674
+ working_dir,
675
+ "rev-parse",
676
+ "--show-toplevel",
515
677
  stdout=asyncio.subprocess.PIPE,
516
- stderr=asyncio.subprocess.PIPE
678
+ stderr=asyncio.subprocess.PIPE,
517
679
  )
518
680
  git_root_output, _ = await git_root_proc.communicate()
519
-
681
+
520
682
  if git_root_proc.returncode != 0:
521
- return {"success": False, "error": "Failed to determine git root directory"}
522
-
683
+ return {
684
+ "success": False,
685
+ "error": "Failed to determine git root directory",
686
+ }
687
+
523
688
  git_root = git_root_output.decode().strip()
524
-
689
+
525
690
  # Make file_path relative to git root if it's absolute
526
- if os.path.isabs(file_path):
691
+ file_path_obj = Path(file_path)
692
+ if file_path_obj.is_absolute():
527
693
  try:
528
- file_path = os.path.relpath(file_path, git_root)
694
+ file_path = str(file_path_obj.relative_to(Path(git_root)))
529
695
  except ValueError:
530
696
  # File is not under git root
531
697
  pass
532
-
698
+
533
699
  # If timestamp is provided, try to find commits around that time
534
700
  if timestamp:
535
701
  # Convert timestamp to git format
536
702
  try:
537
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
538
- git_since = dt.strftime('%Y-%m-%d %H:%M:%S')
539
-
703
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
704
+ git_since = dt.strftime("%Y-%m-%d %H:%M:%S")
705
+
540
706
  # Find commits that modified this file around the timestamp
541
707
  log_proc = await asyncio.create_subprocess_exec(
542
- 'git', '-C', working_dir, 'log', '--oneline', '--since', git_since,
543
- '--until', f'{git_since} +1 hour', '--', file_path,
708
+ "git",
709
+ "-C",
710
+ working_dir,
711
+ "log",
712
+ "--oneline",
713
+ "--since",
714
+ git_since,
715
+ "--until",
716
+ f"{git_since} +1 hour",
717
+ "--",
718
+ file_path,
544
719
  stdout=asyncio.subprocess.PIPE,
545
- stderr=asyncio.subprocess.PIPE
720
+ stderr=asyncio.subprocess.PIPE,
546
721
  )
547
722
  log_output, _ = await log_proc.communicate()
548
-
723
+
549
724
  if log_proc.returncode == 0 and log_output:
550
725
  # Get the most recent commit hash
551
- commits = log_output.decode().strip().split('\n')
726
+ commits = log_output.decode().strip().split("\n")
552
727
  if commits and commits[0]:
553
728
  commit_hash = commits[0].split()[0]
554
-
729
+
555
730
  # Get the diff for this specific commit
556
731
  diff_proc = await asyncio.create_subprocess_exec(
557
- 'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
732
+ "git",
733
+ "-C",
734
+ working_dir,
735
+ "show",
736
+ "--format=fuller",
737
+ commit_hash,
738
+ "--",
739
+ file_path,
558
740
  stdout=asyncio.subprocess.PIPE,
559
- stderr=asyncio.subprocess.PIPE
741
+ stderr=asyncio.subprocess.PIPE,
560
742
  )
561
743
  diff_output, diff_error = await diff_proc.communicate()
562
-
744
+
563
745
  if diff_proc.returncode == 0:
564
746
  return {
565
747
  "success": True,
@@ -567,30 +749,46 @@ class GitEventHandler(BaseEventHandler):
567
749
  "commit_hash": commit_hash,
568
750
  "file_path": file_path,
569
751
  "method": "timestamp_based",
570
- "timestamp": timestamp
752
+ "timestamp": timestamp,
571
753
  }
572
754
  except Exception as e:
573
- self.logger.warning(f"Failed to parse timestamp or find commits: {e}")
574
-
755
+ self.logger.warning(
756
+ f"Failed to parse timestamp or find commits: {e}"
757
+ )
758
+
575
759
  # Fallback: Get the most recent change to the file
576
760
  log_proc = await asyncio.create_subprocess_exec(
577
- 'git', '-C', working_dir, 'log', '-1', '--oneline', '--', file_path,
761
+ "git",
762
+ "-C",
763
+ working_dir,
764
+ "log",
765
+ "-1",
766
+ "--oneline",
767
+ "--",
768
+ file_path,
578
769
  stdout=asyncio.subprocess.PIPE,
579
- stderr=asyncio.subprocess.PIPE
770
+ stderr=asyncio.subprocess.PIPE,
580
771
  )
581
772
  log_output, _ = await log_proc.communicate()
582
-
773
+
583
774
  if log_proc.returncode == 0 and log_output:
584
775
  commit_hash = log_output.decode().strip().split()[0]
585
-
776
+
586
777
  # Get the diff for the most recent commit
587
778
  diff_proc = await asyncio.create_subprocess_exec(
588
- 'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
779
+ "git",
780
+ "-C",
781
+ working_dir,
782
+ "show",
783
+ "--format=fuller",
784
+ commit_hash,
785
+ "--",
786
+ file_path,
589
787
  stdout=asyncio.subprocess.PIPE,
590
- stderr=asyncio.subprocess.PIPE
788
+ stderr=asyncio.subprocess.PIPE,
591
789
  )
592
790
  diff_output, diff_error = await diff_proc.communicate()
593
-
791
+
594
792
  if diff_proc.returncode == 0:
595
793
  return {
596
794
  "success": True,
@@ -598,17 +796,22 @@ class GitEventHandler(BaseEventHandler):
598
796
  "commit_hash": commit_hash,
599
797
  "file_path": file_path,
600
798
  "method": "latest_commit",
601
- "timestamp": timestamp
799
+ "timestamp": timestamp,
602
800
  }
603
-
801
+
604
802
  # Try to show unstaged changes first
605
803
  diff_proc = await asyncio.create_subprocess_exec(
606
- 'git', '-C', working_dir, 'diff', '--', file_path,
804
+ "git",
805
+ "-C",
806
+ working_dir,
807
+ "diff",
808
+ "--",
809
+ file_path,
607
810
  stdout=asyncio.subprocess.PIPE,
608
- stderr=asyncio.subprocess.PIPE
811
+ stderr=asyncio.subprocess.PIPE,
609
812
  )
610
813
  diff_output, _ = await diff_proc.communicate()
611
-
814
+
612
815
  if diff_proc.returncode == 0 and diff_output.decode().strip():
613
816
  return {
614
817
  "success": True,
@@ -616,17 +819,23 @@ class GitEventHandler(BaseEventHandler):
616
819
  "commit_hash": "unstaged_changes",
617
820
  "file_path": file_path,
618
821
  "method": "unstaged_changes",
619
- "timestamp": timestamp
822
+ "timestamp": timestamp,
620
823
  }
621
-
824
+
622
825
  # Then try staged changes
623
826
  diff_proc = await asyncio.create_subprocess_exec(
624
- 'git', '-C', working_dir, 'diff', '--cached', '--', file_path,
827
+ "git",
828
+ "-C",
829
+ working_dir,
830
+ "diff",
831
+ "--cached",
832
+ "--",
833
+ file_path,
625
834
  stdout=asyncio.subprocess.PIPE,
626
- stderr=asyncio.subprocess.PIPE
835
+ stderr=asyncio.subprocess.PIPE,
627
836
  )
628
837
  diff_output, _ = await diff_proc.communicate()
629
-
838
+
630
839
  if diff_proc.returncode == 0 and diff_output.decode().strip():
631
840
  return {
632
841
  "success": True,
@@ -634,17 +843,23 @@ class GitEventHandler(BaseEventHandler):
634
843
  "commit_hash": "staged_changes",
635
844
  "file_path": file_path,
636
845
  "method": "staged_changes",
637
- "timestamp": timestamp
846
+ "timestamp": timestamp,
638
847
  }
639
-
848
+
640
849
  # Final fallback: Show changes against HEAD
641
850
  diff_proc = await asyncio.create_subprocess_exec(
642
- 'git', '-C', working_dir, 'diff', 'HEAD', '--', file_path,
851
+ "git",
852
+ "-C",
853
+ working_dir,
854
+ "diff",
855
+ "HEAD",
856
+ "--",
857
+ file_path,
643
858
  stdout=asyncio.subprocess.PIPE,
644
- stderr=asyncio.subprocess.PIPE
859
+ stderr=asyncio.subprocess.PIPE,
645
860
  )
646
861
  diff_output, _ = await diff_proc.communicate()
647
-
862
+
648
863
  if diff_proc.returncode == 0:
649
864
  working_diff = diff_output.decode()
650
865
  if working_diff.strip():
@@ -654,19 +869,26 @@ class GitEventHandler(BaseEventHandler):
654
869
  "commit_hash": "working_directory",
655
870
  "file_path": file_path,
656
871
  "method": "working_directory",
657
- "timestamp": timestamp
872
+ "timestamp": timestamp,
658
873
  }
659
-
874
+
660
875
  # Check if file is tracked by git
661
876
  status_proc = await asyncio.create_subprocess_exec(
662
- 'git', '-C', working_dir, 'ls-files', '--', file_path,
877
+ "git",
878
+ "-C",
879
+ working_dir,
880
+ "ls-files",
881
+ "--",
882
+ file_path,
663
883
  stdout=asyncio.subprocess.PIPE,
664
- stderr=asyncio.subprocess.PIPE
884
+ stderr=asyncio.subprocess.PIPE,
665
885
  )
666
886
  status_output, _ = await status_proc.communicate()
667
-
668
- is_tracked = status_proc.returncode == 0 and status_output.decode().strip()
669
-
887
+
888
+ is_tracked = (
889
+ status_proc.returncode == 0 and status_output.decode().strip()
890
+ )
891
+
670
892
  if not is_tracked:
671
893
  # File is not tracked by git
672
894
  return {
@@ -677,47 +899,59 @@ class GitEventHandler(BaseEventHandler):
677
899
  "suggestions": [
678
900
  "This file has not been added to git yet",
679
901
  "Use 'git add' to track this file before viewing its diff",
680
- "Git diff can only show changes for files that are tracked by git"
681
- ]
902
+ "Git diff can only show changes for files that are tracked by git",
903
+ ],
682
904
  }
683
-
905
+
684
906
  # File is tracked but has no changes to show
685
907
  suggestions = [
686
908
  "The file may not have any committed changes yet",
687
909
  "The file may have been added but not committed",
688
- "The timestamp may be outside the git history range"
910
+ "The timestamp may be outside the git history range",
689
911
  ]
690
-
691
- if os.path.isabs(file_path) and not file_path.startswith(os.getcwd()):
692
- current_repo = os.path.basename(os.getcwd())
912
+
913
+ file_path_obj = Path(file_path)
914
+ current_cwd = Path.cwd()
915
+ if file_path_obj.is_absolute() and not str(file_path_obj).startswith(
916
+ str(current_cwd)
917
+ ):
918
+ current_repo = current_cwd.name
693
919
  file_repo = "unknown"
694
920
  # Try to extract repository name from path
695
- path_parts = file_path.split("/")
921
+ path_parts = file_path_obj.parts
696
922
  if "Projects" in path_parts:
697
923
  idx = path_parts.index("Projects")
698
924
  if idx + 1 < len(path_parts):
699
925
  file_repo = path_parts[idx + 1]
700
-
926
+
701
927
  suggestions.clear()
702
- suggestions.append(f"This file is from the '{file_repo}' repository")
703
- suggestions.append(f"The git diff viewer is running from the '{current_repo}' repository")
704
- suggestions.append("Git diff can only show changes for files in the current repository")
705
- suggestions.append("To view changes for this file, run the monitoring dashboard from its repository")
706
-
928
+ suggestions.append(
929
+ f"This file is from the '{file_repo}' repository"
930
+ )
931
+ suggestions.append(
932
+ f"The git diff viewer is running from the '{current_repo}' repository"
933
+ )
934
+ suggestions.append(
935
+ "Git diff can only show changes for files in the current repository"
936
+ )
937
+ suggestions.append(
938
+ "To view changes for this file, run the monitoring dashboard from its repository"
939
+ )
940
+
707
941
  return {
708
942
  "success": False,
709
943
  "error": "No git history found for this file",
710
944
  "file_path": file_path,
711
- "suggestions": suggestions
945
+ "suggestions": suggestions,
712
946
  }
713
-
947
+
714
948
  finally:
715
949
  os.chdir(original_cwd)
716
-
950
+
717
951
  except Exception as e:
718
952
  self.logger.error(f"Error in generate_git_diff: {e}")
719
953
  return {
720
954
  "success": False,
721
955
  "error": f"Git diff generation failed: {str(e)}",
722
- "file_path": file_path
723
- }
956
+ "file_path": file_path,
957
+ }