claude-mpm 3.7.8__py3-none-any.whl → 3.9.0__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/agents/BASE_PM.md +0 -106
- claude_mpm/agents/INSTRUCTIONS.md +0 -96
- claude_mpm/agents/MEMORY.md +94 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +3 -8
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/agents.py +8 -3
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +217 -5
- claude_mpm/config/__init__.py +30 -39
- claude_mpm/config/socketio_config.py +8 -5
- claude_mpm/constants.py +13 -0
- claude_mpm/core/__init__.py +8 -18
- claude_mpm/core/cache.py +596 -0
- claude_mpm/core/claude_runner.py +166 -622
- claude_mpm/core/config.py +7 -3
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +548 -38
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +249 -93
- claude_mpm/core/interactive_session.py +479 -0
- claude_mpm/core/interfaces.py +424 -0
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/logging_config.py +444 -0
- claude_mpm/core/oneshot_session.py +465 -0
- claude_mpm/core/optimized_agent_loader.py +485 -0
- claude_mpm/core/optimized_startup.py +490 -0
- claude_mpm/core/service_registry.py +52 -26
- claude_mpm/core/socketio_pool.py +162 -5
- claude_mpm/core/types.py +292 -0
- claude_mpm/core/typing_utils.py +477 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
- claude_mpm/init.py +2 -1
- claude_mpm/services/__init__.py +78 -14
- claude_mpm/services/agent/__init__.py +24 -0
- claude_mpm/services/agent/deployment.py +2548 -0
- claude_mpm/services/agent/management.py +598 -0
- claude_mpm/services/agent/registry.py +813 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +728 -308
- claude_mpm/services/agents/memory/agent_memory_manager.py +160 -4
- claude_mpm/services/async_session_logger.py +8 -3
- claude_mpm/services/communication/__init__.py +21 -0
- claude_mpm/services/communication/socketio.py +1933 -0
- claude_mpm/services/communication/websocket.py +479 -0
- claude_mpm/services/core/__init__.py +123 -0
- claude_mpm/services/core/base.py +247 -0
- claude_mpm/services/core/interfaces.py +951 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
- claude_mpm/services/framework_claude_md_generator.py +3 -2
- claude_mpm/services/health_monitor.py +4 -3
- claude_mpm/services/hook_service.py +64 -4
- claude_mpm/services/infrastructure/__init__.py +21 -0
- claude_mpm/services/infrastructure/logging.py +202 -0
- claude_mpm/services/infrastructure/monitoring.py +893 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/project/__init__.py +21 -0
- claude_mpm/services/project/analyzer.py +864 -0
- claude_mpm/services/project/registry.py +608 -0
- claude_mpm/services/project_analyzer.py +95 -2
- claude_mpm/services/recovery_manager.py +15 -9
- claude_mpm/services/response_tracker.py +3 -5
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/base.py +121 -0
- claude_mpm/services/socketio/handlers/connection.py +198 -0
- claude_mpm/services/socketio/handlers/file.py +213 -0
- claude_mpm/services/socketio/handlers/git.py +723 -0
- claude_mpm/services/socketio/handlers/memory.py +27 -0
- claude_mpm/services/socketio/handlers/project.py +25 -0
- claude_mpm/services/socketio/handlers/registry.py +145 -0
- claude_mpm/services/socketio_client_manager.py +12 -7
- claude_mpm/services/socketio_server.py +156 -30
- claude_mpm/services/ticket_manager.py +172 -9
- claude_mpm/services/ticket_manager_di.py +1 -1
- claude_mpm/services/version_control/semantic_versioning.py +80 -7
- claude_mpm/services/version_control/version_parser.py +528 -0
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/METADATA +38 -128
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/RECORD +100 -59
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,723 @@ | |
| 1 | 
            +
            """Git operation event handlers for Socket.IO.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            WHY: This module handles all git-related events including branch queries,
         | 
| 4 | 
            +
            file tracking status, and git add operations. Isolating git operations
         | 
| 5 | 
            +
            improves maintainability and makes it easier to extend git functionality.
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            import os
         | 
| 9 | 
            +
            import subprocess
         | 
| 10 | 
            +
            import asyncio
         | 
| 11 | 
            +
            from typing import Optional, Dict, Any, List
         | 
| 12 | 
            +
            from datetime import datetime
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            from .base import BaseEventHandler
         | 
| 15 | 
            +
            from ....core.typing_utils import SocketId, EventData, PathLike
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
            class GitEventHandler(BaseEventHandler):
         | 
| 19 | 
            +
                """Handles git-related Socket.IO events.
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                WHY: Git operations are a distinct domain that benefits from focused
         | 
| 22 | 
            +
                handling. This includes checking branches, file tracking status,
         | 
| 23 | 
            +
                and adding files to git. Separating these improves code organization.
         | 
| 24 | 
            +
                """
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                def register_events(self) -> None:
         | 
| 27 | 
            +
                    """Register git-related event handlers."""
         | 
| 28 | 
            +
                    
         | 
| 29 | 
            +
                    @self.sio.event
         | 
| 30 | 
            +
                    async def get_git_branch(sid, working_dir=None):
         | 
| 31 | 
            +
                        """Get the current git branch for a directory.
         | 
| 32 | 
            +
                        
         | 
| 33 | 
            +
                        WHY: The dashboard needs to display the current git branch
         | 
| 34 | 
            +
                        to provide context about which branch changes are being made on.
         | 
| 35 | 
            +
                        """
         | 
| 36 | 
            +
                        try:
         | 
| 37 | 
            +
                            self.logger.info(f"[GIT-BRANCH-DEBUG] get_git_branch called with working_dir: {repr(working_dir)} (type: {type(working_dir)})")
         | 
| 38 | 
            +
                            
         | 
| 39 | 
            +
                            # Validate and sanitize working directory
         | 
| 40 | 
            +
                            working_dir = self._sanitize_working_dir(working_dir, "get_git_branch")
         | 
| 41 | 
            +
                            
         | 
| 42 | 
            +
                            if not self._validate_directory(sid, working_dir, "git_branch_response"):
         | 
| 43 | 
            +
                                return
         | 
| 44 | 
            +
                            
         | 
| 45 | 
            +
                            self.logger.info(f"[GIT-BRANCH-DEBUG] Running git command in directory: {working_dir}")
         | 
| 46 | 
            +
                            
         | 
| 47 | 
            +
                            # Run git command to get current branch
         | 
| 48 | 
            +
                            result = subprocess.run(
         | 
| 49 | 
            +
                                ["git", "rev-parse", "--abbrev-ref", "HEAD"],
         | 
| 50 | 
            +
                                cwd=working_dir,
         | 
| 51 | 
            +
                                capture_output=True,
         | 
| 52 | 
            +
                                text=True
         | 
| 53 | 
            +
                            )
         | 
| 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 | 
            +
                            if result.returncode == 0:
         | 
| 58 | 
            +
                                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 | 
            +
                                })
         | 
| 66 | 
            +
                            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 | 
            +
                                
         | 
| 76 | 
            +
                        except Exception as e:
         | 
| 77 | 
            +
                            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 | 
            +
                    
         | 
| 85 | 
            +
                    @self.sio.event
         | 
| 86 | 
            +
                    async def check_file_tracked(sid, data):
         | 
| 87 | 
            +
                        """Check if a file is tracked by git.
         | 
| 88 | 
            +
                        
         | 
| 89 | 
            +
                        WHY: The dashboard needs to know if a file is tracked by git
         | 
| 90 | 
            +
                        to determine whether to show git-related UI elements.
         | 
| 91 | 
            +
                        """
         | 
| 92 | 
            +
                        try:
         | 
| 93 | 
            +
                            file_path = data.get('file_path')
         | 
| 94 | 
            +
                            working_dir = data.get('working_dir', os.getcwd())
         | 
| 95 | 
            +
                            
         | 
| 96 | 
            +
                            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 | 
            +
                                })
         | 
| 102 | 
            +
                                return
         | 
| 103 | 
            +
                            
         | 
| 104 | 
            +
                            # Use git ls-files to check if file is tracked
         | 
| 105 | 
            +
                            result = subprocess.run(
         | 
| 106 | 
            +
                                ["git", "-C", working_dir, "ls-files", "--", file_path],
         | 
| 107 | 
            +
                                capture_output=True,
         | 
| 108 | 
            +
                                text=True
         | 
| 109 | 
            +
                            )
         | 
| 110 | 
            +
                            
         | 
| 111 | 
            +
                            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 | 
            +
                                
         | 
| 120 | 
            +
                        except Exception as e:
         | 
| 121 | 
            +
                            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 | 
            +
                    
         | 
| 128 | 
            +
                    @self.sio.event
         | 
| 129 | 
            +
                    async def check_git_status(sid, data):
         | 
| 130 | 
            +
                        """Check git status for a file to determine if git diff icons should be shown.
         | 
| 131 | 
            +
                        
         | 
| 132 | 
            +
                        WHY: The dashboard shows git diff icons for files that have changes.
         | 
| 133 | 
            +
                        This checks if a file has git status to determine icon visibility.
         | 
| 134 | 
            +
                        """
         | 
| 135 | 
            +
                        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 | 
            +
                            
         | 
| 141 | 
            +
                            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 | 
            +
                                })
         | 
| 147 | 
            +
                                return
         | 
| 148 | 
            +
                            
         | 
| 149 | 
            +
                            # Validate and sanitize working_dir
         | 
| 150 | 
            +
                            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):
         | 
| 154 | 
            +
                                return
         | 
| 155 | 
            +
                            
         | 
| 156 | 
            +
                            # Check if this is a git repository
         | 
| 157 | 
            +
                            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 | 
            +
                                })
         | 
| 165 | 
            +
                                return
         | 
| 166 | 
            +
                            
         | 
| 167 | 
            +
                            # Check git status for the file
         | 
| 168 | 
            +
                            file_path_for_git = self._make_path_relative_to_git(file_path, working_dir)
         | 
| 169 | 
            +
                            
         | 
| 170 | 
            +
                            # 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 | 
            +
                                })
         | 
| 181 | 
            +
                                return
         | 
| 182 | 
            +
                            
         | 
| 183 | 
            +
                            # Check git status and tracking
         | 
| 184 | 
            +
                            is_tracked, has_changes = self._check_file_git_status(file_path_for_git, working_dir)
         | 
| 185 | 
            +
                            
         | 
| 186 | 
            +
                            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 | 
            +
                                })
         | 
| 196 | 
            +
                            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 | 
            +
                                
         | 
| 207 | 
            +
                        except Exception as e:
         | 
| 208 | 
            +
                            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 | 
            +
                    
         | 
| 216 | 
            +
                    @self.sio.event
         | 
| 217 | 
            +
                    async def git_add_file(sid, data):
         | 
| 218 | 
            +
                        """Add file to git tracking.
         | 
| 219 | 
            +
                        
         | 
| 220 | 
            +
                        WHY: Users can add untracked files to git directly from the dashboard,
         | 
| 221 | 
            +
                        making it easier to manage version control without leaving the UI.
         | 
| 222 | 
            +
                        """
         | 
| 223 | 
            +
                        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 | 
            +
                            
         | 
| 229 | 
            +
                            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 | 
            +
                                })
         | 
| 235 | 
            +
                                return
         | 
| 236 | 
            +
                            
         | 
| 237 | 
            +
                            # Validate and sanitize working_dir
         | 
| 238 | 
            +
                            original_working_dir = working_dir
         | 
| 239 | 
            +
                            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):
         | 
| 242 | 
            +
                                return
         | 
| 243 | 
            +
                            
         | 
| 244 | 
            +
                            self.logger.info(f"[GIT-ADD-DEBUG] Running git add command in directory: {working_dir}")
         | 
| 245 | 
            +
                            
         | 
| 246 | 
            +
                            # Use git add to track the file
         | 
| 247 | 
            +
                            result = subprocess.run(
         | 
| 248 | 
            +
                                ["git", "-C", working_dir, "add", file_path],
         | 
| 249 | 
            +
                                capture_output=True,
         | 
| 250 | 
            +
                                text=True
         | 
| 251 | 
            +
                            )
         | 
| 252 | 
            +
                            
         | 
| 253 | 
            +
                            self.logger.info(f"[GIT-ADD-DEBUG] Git add result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
         | 
| 254 | 
            +
                            
         | 
| 255 | 
            +
                            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 | 
            +
                                })
         | 
| 264 | 
            +
                            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 | 
            +
                                
         | 
| 275 | 
            +
                        except Exception as e:
         | 
| 276 | 
            +
                            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 | 
            +
                
         | 
| 284 | 
            +
                def _sanitize_working_dir(self, working_dir: Optional[str], operation: str) -> str:
         | 
| 285 | 
            +
                    """Sanitize and validate working directory input.
         | 
| 286 | 
            +
                    
         | 
| 287 | 
            +
                    WHY: Working directory input from clients can be invalid or malformed.
         | 
| 288 | 
            +
                    This ensures we have a valid directory path to work with.
         | 
| 289 | 
            +
                    """
         | 
| 290 | 
            +
                    invalid_states = [
         | 
| 291 | 
            +
                        None, '', 'Unknown', 'Loading...', 'Loading', 'undefined', 'null', 
         | 
| 292 | 
            +
                        'Not Connected', 'Invalid Directory', 'No Directory', '.'
         | 
| 293 | 
            +
                    ]
         | 
| 294 | 
            +
                    
         | 
| 295 | 
            +
                    original_working_dir = working_dir
         | 
| 296 | 
            +
                    if working_dir in invalid_states or (isinstance(working_dir, str) and working_dir.strip() == ''):
         | 
| 297 | 
            +
                        working_dir = os.getcwd()
         | 
| 298 | 
            +
                        self.logger.info(f"[{operation}] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
         | 
| 299 | 
            +
                    else:
         | 
| 300 | 
            +
                        self.logger.info(f"[{operation}] Using provided working_dir: {working_dir}")
         | 
| 301 | 
            +
                    
         | 
| 302 | 
            +
                    # Additional validation for obviously invalid paths
         | 
| 303 | 
            +
                    if isinstance(working_dir, str):
         | 
| 304 | 
            +
                        working_dir = working_dir.strip()
         | 
| 305 | 
            +
                        # 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")
         | 
| 308 | 
            +
                            working_dir = os.getcwd()
         | 
| 309 | 
            +
                    
         | 
| 310 | 
            +
                    return working_dir
         | 
| 311 | 
            +
                
         | 
| 312 | 
            +
                async def _validate_directory(self, sid: str, working_dir: str, response_event: str) -> bool:
         | 
| 313 | 
            +
                    """Validate that a directory exists and is accessible.
         | 
| 314 | 
            +
                    
         | 
| 315 | 
            +
                    WHY: We need to ensure the directory exists and is a directory
         | 
| 316 | 
            +
                    before attempting git operations on it.
         | 
| 317 | 
            +
                    """
         | 
| 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 | 
            +
                        })
         | 
| 326 | 
            +
                        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 | 
            +
                        })
         | 
| 336 | 
            +
                        return False
         | 
| 337 | 
            +
                    
         | 
| 338 | 
            +
                    return True
         | 
| 339 | 
            +
                
         | 
| 340 | 
            +
                async def _validate_directory_for_status(self, sid: str, working_dir: str, original_working_dir: str, file_path: str) -> bool:
         | 
| 341 | 
            +
                    """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 | 
            +
                        })
         | 
| 351 | 
            +
                        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 | 
            +
                        })
         | 
| 362 | 
            +
                        return False
         | 
| 363 | 
            +
                    
         | 
| 364 | 
            +
                    return True
         | 
| 365 | 
            +
                
         | 
| 366 | 
            +
                async def _validate_directory_for_add(self, sid: str, working_dir: str, original_working_dir: str, file_path: str) -> bool:
         | 
| 367 | 
            +
                    """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 | 
            +
                        })
         | 
| 377 | 
            +
                        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 | 
            +
                        })
         | 
| 388 | 
            +
                        return False
         | 
| 389 | 
            +
                    
         | 
| 390 | 
            +
                    return True
         | 
| 391 | 
            +
                
         | 
| 392 | 
            +
                def _is_git_repository(self, working_dir: str) -> bool:
         | 
| 393 | 
            +
                    """Check if a directory is a git repository."""
         | 
| 394 | 
            +
                    git_check = subprocess.run(
         | 
| 395 | 
            +
                        ["git", "-C", working_dir, "rev-parse", "--git-dir"],
         | 
| 396 | 
            +
                        capture_output=True,
         | 
| 397 | 
            +
                        text=True
         | 
| 398 | 
            +
                    )
         | 
| 399 | 
            +
                    return git_check.returncode == 0
         | 
| 400 | 
            +
                
         | 
| 401 | 
            +
                def _make_path_relative_to_git(self, file_path: str, working_dir: str) -> str:
         | 
| 402 | 
            +
                    """Make an absolute path relative to the git root if needed."""
         | 
| 403 | 
            +
                    if not os.path.isabs(file_path):
         | 
| 404 | 
            +
                        return file_path
         | 
| 405 | 
            +
                    
         | 
| 406 | 
            +
                    # Get git root to make path relative if needed
         | 
| 407 | 
            +
                    git_root_result = subprocess.run(
         | 
| 408 | 
            +
                        ["git", "-C", working_dir, "rev-parse", "--show-toplevel"],
         | 
| 409 | 
            +
                        capture_output=True,
         | 
| 410 | 
            +
                        text=True
         | 
| 411 | 
            +
                    )
         | 
| 412 | 
            +
                    
         | 
| 413 | 
            +
                    if git_root_result.returncode == 0:
         | 
| 414 | 
            +
                        git_root = git_root_result.stdout.strip()
         | 
| 415 | 
            +
                        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
         | 
| 419 | 
            +
                        except ValueError:
         | 
| 420 | 
            +
                            # 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 | 
            +
                    
         | 
| 423 | 
            +
                    return file_path
         | 
| 424 | 
            +
                
         | 
| 425 | 
            +
                def _check_file_git_status(self, file_path: str, working_dir: str) -> tuple[bool, bool]:
         | 
| 426 | 
            +
                    """Check if a file is tracked and has changes."""
         | 
| 427 | 
            +
                    # Check git status for the file
         | 
| 428 | 
            +
                    git_status_result = subprocess.run(
         | 
| 429 | 
            +
                        ["git", "-C", working_dir, "status", "--porcelain", file_path],
         | 
| 430 | 
            +
                        capture_output=True,
         | 
| 431 | 
            +
                        text=True
         | 
| 432 | 
            +
                    )
         | 
| 433 | 
            +
                    
         | 
| 434 | 
            +
                    # Check if file is tracked by git
         | 
| 435 | 
            +
                    ls_files_result = subprocess.run(
         | 
| 436 | 
            +
                        ["git", "-C", working_dir, "ls-files", file_path],
         | 
| 437 | 
            +
                        capture_output=True,
         | 
| 438 | 
            +
                        text=True
         | 
| 439 | 
            +
                    )
         | 
| 440 | 
            +
                    
         | 
| 441 | 
            +
                    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 | 
            +
                    
         | 
| 446 | 
            +
                    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]:
         | 
| 449 | 
            +
                    """Generate git diff for a specific file operation.
         | 
| 450 | 
            +
                    
         | 
| 451 | 
            +
                    WHY: This method generates a git diff showing the changes made to a file
         | 
| 452 | 
            +
                    during a specific write operation. It uses git log and show commands to
         | 
| 453 | 
            +
                    find the most relevant commit around the specified timestamp.
         | 
| 454 | 
            +
                    
         | 
| 455 | 
            +
                    Args:
         | 
| 456 | 
            +
                        file_path: Path to the file relative to the git repository
         | 
| 457 | 
            +
                        timestamp: ISO timestamp of the file operation (optional)
         | 
| 458 | 
            +
                        working_dir: Working directory containing the git repository
         | 
| 459 | 
            +
                        
         | 
| 460 | 
            +
                    Returns:
         | 
| 461 | 
            +
                        dict: Contains diff content, metadata, and status information
         | 
| 462 | 
            +
                    """
         | 
| 463 | 
            +
                    try:
         | 
| 464 | 
            +
                        # If file_path is absolute, determine its git repository
         | 
| 465 | 
            +
                        if os.path.isabs(file_path):
         | 
| 466 | 
            +
                            # Find the directory containing the file
         | 
| 467 | 
            +
                            file_dir = os.path.dirname(file_path)
         | 
| 468 | 
            +
                            if os.path.exists(file_dir):
         | 
| 469 | 
            +
                                # Try to find the git root from the file's directory
         | 
| 470 | 
            +
                                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
         | 
| 474 | 
            +
                                        self.logger.info(f"Found git repository at: {working_dir}")
         | 
| 475 | 
            +
                                        break
         | 
| 476 | 
            +
                                    current_dir = os.path.dirname(current_dir)
         | 
| 477 | 
            +
                                else:
         | 
| 478 | 
            +
                                    # 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 | 
            +
                        
         | 
| 482 | 
            +
                        # Handle case where working_dir is None, empty string, or 'Unknown'
         | 
| 483 | 
            +
                        original_working_dir = working_dir
         | 
| 484 | 
            +
                        if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '':
         | 
| 485 | 
            +
                            working_dir = os.getcwd()
         | 
| 486 | 
            +
                            self.logger.info(f"[GIT-DIFF-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
         | 
| 487 | 
            +
                        else:
         | 
| 488 | 
            +
                            self.logger.info(f"[GIT-DIFF-DEBUG] Using provided working_dir: {working_dir}")
         | 
| 489 | 
            +
                            
         | 
| 490 | 
            +
                        # For read-only git operations, we can work from any directory
         | 
| 491 | 
            +
                        # by passing the -C flag to git commands instead of changing directories
         | 
| 492 | 
            +
                        original_cwd = os.getcwd()
         | 
| 493 | 
            +
                        try:
         | 
| 494 | 
            +
                            # We'll use git -C <working_dir> for all commands instead of chdir
         | 
| 495 | 
            +
                            
         | 
| 496 | 
            +
                            # Check if this is a git repository
         | 
| 497 | 
            +
                            git_check = await asyncio.create_subprocess_exec(
         | 
| 498 | 
            +
                                'git', '-C', working_dir, 'rev-parse', '--git-dir',
         | 
| 499 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 500 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 501 | 
            +
                            )
         | 
| 502 | 
            +
                            await git_check.communicate()
         | 
| 503 | 
            +
                            
         | 
| 504 | 
            +
                            if git_check.returncode != 0:
         | 
| 505 | 
            +
                                return {
         | 
| 506 | 
            +
                                    "success": False,
         | 
| 507 | 
            +
                                    "error": "Not a git repository",
         | 
| 508 | 
            +
                                    "file_path": file_path,
         | 
| 509 | 
            +
                                    "working_dir": working_dir
         | 
| 510 | 
            +
                                }
         | 
| 511 | 
            +
                            
         | 
| 512 | 
            +
                            # Get the absolute path of the file relative to git root
         | 
| 513 | 
            +
                            git_root_proc = await asyncio.create_subprocess_exec(
         | 
| 514 | 
            +
                                'git', '-C', working_dir, 'rev-parse', '--show-toplevel',
         | 
| 515 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 516 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 517 | 
            +
                            )
         | 
| 518 | 
            +
                            git_root_output, _ = await git_root_proc.communicate()
         | 
| 519 | 
            +
                            
         | 
| 520 | 
            +
                            if git_root_proc.returncode != 0:
         | 
| 521 | 
            +
                                return {"success": False, "error": "Failed to determine git root directory"}
         | 
| 522 | 
            +
                            
         | 
| 523 | 
            +
                            git_root = git_root_output.decode().strip()
         | 
| 524 | 
            +
                            
         | 
| 525 | 
            +
                            # Make file_path relative to git root if it's absolute
         | 
| 526 | 
            +
                            if os.path.isabs(file_path):
         | 
| 527 | 
            +
                                try:
         | 
| 528 | 
            +
                                    file_path = os.path.relpath(file_path, git_root)
         | 
| 529 | 
            +
                                except ValueError:
         | 
| 530 | 
            +
                                    # File is not under git root
         | 
| 531 | 
            +
                                    pass
         | 
| 532 | 
            +
                            
         | 
| 533 | 
            +
                            # If timestamp is provided, try to find commits around that time
         | 
| 534 | 
            +
                            if timestamp:
         | 
| 535 | 
            +
                                # Convert timestamp to git format
         | 
| 536 | 
            +
                                try:
         | 
| 537 | 
            +
                                    dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
         | 
| 538 | 
            +
                                    git_since = dt.strftime('%Y-%m-%d %H:%M:%S')
         | 
| 539 | 
            +
                                    
         | 
| 540 | 
            +
                                    # Find commits that modified this file around the timestamp
         | 
| 541 | 
            +
                                    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,
         | 
| 544 | 
            +
                                        stdout=asyncio.subprocess.PIPE,
         | 
| 545 | 
            +
                                        stderr=asyncio.subprocess.PIPE
         | 
| 546 | 
            +
                                    )
         | 
| 547 | 
            +
                                    log_output, _ = await log_proc.communicate()
         | 
| 548 | 
            +
                                    
         | 
| 549 | 
            +
                                    if log_proc.returncode == 0 and log_output:
         | 
| 550 | 
            +
                                        # Get the most recent commit hash
         | 
| 551 | 
            +
                                        commits = log_output.decode().strip().split('\n')
         | 
| 552 | 
            +
                                        if commits and commits[0]:
         | 
| 553 | 
            +
                                            commit_hash = commits[0].split()[0]
         | 
| 554 | 
            +
                                            
         | 
| 555 | 
            +
                                            # Get the diff for this specific commit
         | 
| 556 | 
            +
                                            diff_proc = await asyncio.create_subprocess_exec(
         | 
| 557 | 
            +
                                                'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
         | 
| 558 | 
            +
                                                stdout=asyncio.subprocess.PIPE,
         | 
| 559 | 
            +
                                                stderr=asyncio.subprocess.PIPE
         | 
| 560 | 
            +
                                            )
         | 
| 561 | 
            +
                                            diff_output, diff_error = await diff_proc.communicate()
         | 
| 562 | 
            +
                                            
         | 
| 563 | 
            +
                                            if diff_proc.returncode == 0:
         | 
| 564 | 
            +
                                                return {
         | 
| 565 | 
            +
                                                    "success": True,
         | 
| 566 | 
            +
                                                    "diff": diff_output.decode(),
         | 
| 567 | 
            +
                                                    "commit_hash": commit_hash,
         | 
| 568 | 
            +
                                                    "file_path": file_path,
         | 
| 569 | 
            +
                                                    "method": "timestamp_based",
         | 
| 570 | 
            +
                                                    "timestamp": timestamp
         | 
| 571 | 
            +
                                                }
         | 
| 572 | 
            +
                                except Exception as e:
         | 
| 573 | 
            +
                                    self.logger.warning(f"Failed to parse timestamp or find commits: {e}")
         | 
| 574 | 
            +
                            
         | 
| 575 | 
            +
                            # Fallback: Get the most recent change to the file
         | 
| 576 | 
            +
                            log_proc = await asyncio.create_subprocess_exec(
         | 
| 577 | 
            +
                                'git', '-C', working_dir, 'log', '-1', '--oneline', '--', file_path,
         | 
| 578 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 579 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 580 | 
            +
                            )
         | 
| 581 | 
            +
                            log_output, _ = await log_proc.communicate()
         | 
| 582 | 
            +
                            
         | 
| 583 | 
            +
                            if log_proc.returncode == 0 and log_output:
         | 
| 584 | 
            +
                                commit_hash = log_output.decode().strip().split()[0]
         | 
| 585 | 
            +
                                
         | 
| 586 | 
            +
                                # Get the diff for the most recent commit
         | 
| 587 | 
            +
                                diff_proc = await asyncio.create_subprocess_exec(
         | 
| 588 | 
            +
                                    'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
         | 
| 589 | 
            +
                                    stdout=asyncio.subprocess.PIPE,
         | 
| 590 | 
            +
                                    stderr=asyncio.subprocess.PIPE
         | 
| 591 | 
            +
                                )
         | 
| 592 | 
            +
                                diff_output, diff_error = await diff_proc.communicate()
         | 
| 593 | 
            +
                                
         | 
| 594 | 
            +
                                if diff_proc.returncode == 0:
         | 
| 595 | 
            +
                                    return {
         | 
| 596 | 
            +
                                        "success": True,
         | 
| 597 | 
            +
                                        "diff": diff_output.decode(),
         | 
| 598 | 
            +
                                        "commit_hash": commit_hash,
         | 
| 599 | 
            +
                                        "file_path": file_path,
         | 
| 600 | 
            +
                                        "method": "latest_commit",
         | 
| 601 | 
            +
                                        "timestamp": timestamp
         | 
| 602 | 
            +
                                    }
         | 
| 603 | 
            +
                            
         | 
| 604 | 
            +
                            # Try to show unstaged changes first
         | 
| 605 | 
            +
                            diff_proc = await asyncio.create_subprocess_exec(
         | 
| 606 | 
            +
                                'git', '-C', working_dir, 'diff', '--', file_path,
         | 
| 607 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 608 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 609 | 
            +
                            )
         | 
| 610 | 
            +
                            diff_output, _ = await diff_proc.communicate()
         | 
| 611 | 
            +
                            
         | 
| 612 | 
            +
                            if diff_proc.returncode == 0 and diff_output.decode().strip():
         | 
| 613 | 
            +
                                return {
         | 
| 614 | 
            +
                                    "success": True,
         | 
| 615 | 
            +
                                    "diff": diff_output.decode(),
         | 
| 616 | 
            +
                                    "commit_hash": "unstaged_changes",
         | 
| 617 | 
            +
                                    "file_path": file_path,
         | 
| 618 | 
            +
                                    "method": "unstaged_changes",
         | 
| 619 | 
            +
                                    "timestamp": timestamp
         | 
| 620 | 
            +
                                }
         | 
| 621 | 
            +
                            
         | 
| 622 | 
            +
                            # Then try staged changes
         | 
| 623 | 
            +
                            diff_proc = await asyncio.create_subprocess_exec(
         | 
| 624 | 
            +
                                'git', '-C', working_dir, 'diff', '--cached', '--', file_path,
         | 
| 625 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 626 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 627 | 
            +
                            )
         | 
| 628 | 
            +
                            diff_output, _ = await diff_proc.communicate()
         | 
| 629 | 
            +
                            
         | 
| 630 | 
            +
                            if diff_proc.returncode == 0 and diff_output.decode().strip():
         | 
| 631 | 
            +
                                return {
         | 
| 632 | 
            +
                                    "success": True,
         | 
| 633 | 
            +
                                    "diff": diff_output.decode(),
         | 
| 634 | 
            +
                                    "commit_hash": "staged_changes",
         | 
| 635 | 
            +
                                    "file_path": file_path,
         | 
| 636 | 
            +
                                    "method": "staged_changes",
         | 
| 637 | 
            +
                                    "timestamp": timestamp
         | 
| 638 | 
            +
                                }
         | 
| 639 | 
            +
                            
         | 
| 640 | 
            +
                            # Final fallback: Show changes against HEAD
         | 
| 641 | 
            +
                            diff_proc = await asyncio.create_subprocess_exec(
         | 
| 642 | 
            +
                                'git', '-C', working_dir, 'diff', 'HEAD', '--', file_path,
         | 
| 643 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 644 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 645 | 
            +
                            )
         | 
| 646 | 
            +
                            diff_output, _ = await diff_proc.communicate()
         | 
| 647 | 
            +
                            
         | 
| 648 | 
            +
                            if diff_proc.returncode == 0:
         | 
| 649 | 
            +
                                working_diff = diff_output.decode()
         | 
| 650 | 
            +
                                if working_diff.strip():
         | 
| 651 | 
            +
                                    return {
         | 
| 652 | 
            +
                                        "success": True,
         | 
| 653 | 
            +
                                        "diff": working_diff,
         | 
| 654 | 
            +
                                        "commit_hash": "working_directory",
         | 
| 655 | 
            +
                                        "file_path": file_path,
         | 
| 656 | 
            +
                                        "method": "working_directory",
         | 
| 657 | 
            +
                                        "timestamp": timestamp
         | 
| 658 | 
            +
                                    }
         | 
| 659 | 
            +
                            
         | 
| 660 | 
            +
                            # Check if file is tracked by git
         | 
| 661 | 
            +
                            status_proc = await asyncio.create_subprocess_exec(
         | 
| 662 | 
            +
                                'git', '-C', working_dir, 'ls-files', '--', file_path,
         | 
| 663 | 
            +
                                stdout=asyncio.subprocess.PIPE,
         | 
| 664 | 
            +
                                stderr=asyncio.subprocess.PIPE
         | 
| 665 | 
            +
                            )
         | 
| 666 | 
            +
                            status_output, _ = await status_proc.communicate()
         | 
| 667 | 
            +
                            
         | 
| 668 | 
            +
                            is_tracked = status_proc.returncode == 0 and status_output.decode().strip()
         | 
| 669 | 
            +
                            
         | 
| 670 | 
            +
                            if not is_tracked:
         | 
| 671 | 
            +
                                # File is not tracked by git
         | 
| 672 | 
            +
                                return {
         | 
| 673 | 
            +
                                    "success": False,
         | 
| 674 | 
            +
                                    "error": "This file is not tracked by git",
         | 
| 675 | 
            +
                                    "file_path": file_path,
         | 
| 676 | 
            +
                                    "working_dir": working_dir,
         | 
| 677 | 
            +
                                    "suggestions": [
         | 
| 678 | 
            +
                                        "This file has not been added to git yet",
         | 
| 679 | 
            +
                                        "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 | 
            +
                                    ]
         | 
| 682 | 
            +
                                }
         | 
| 683 | 
            +
                            
         | 
| 684 | 
            +
                            # File is tracked but has no changes to show
         | 
| 685 | 
            +
                            suggestions = [
         | 
| 686 | 
            +
                                "The file may not have any committed changes yet",
         | 
| 687 | 
            +
                                "The file may have been added but not committed",
         | 
| 688 | 
            +
                                "The timestamp may be outside the git history range"
         | 
| 689 | 
            +
                            ]
         | 
| 690 | 
            +
                            
         | 
| 691 | 
            +
                            if os.path.isabs(file_path) and not file_path.startswith(os.getcwd()):
         | 
| 692 | 
            +
                                current_repo = os.path.basename(os.getcwd())
         | 
| 693 | 
            +
                                file_repo = "unknown"
         | 
| 694 | 
            +
                                # Try to extract repository name from path
         | 
| 695 | 
            +
                                path_parts = file_path.split("/")
         | 
| 696 | 
            +
                                if "Projects" in path_parts:
         | 
| 697 | 
            +
                                    idx = path_parts.index("Projects")
         | 
| 698 | 
            +
                                    if idx + 1 < len(path_parts):
         | 
| 699 | 
            +
                                        file_repo = path_parts[idx + 1]
         | 
| 700 | 
            +
                                
         | 
| 701 | 
            +
                                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 | 
            +
                            
         | 
| 707 | 
            +
                            return {
         | 
| 708 | 
            +
                                "success": False,
         | 
| 709 | 
            +
                                "error": "No git history found for this file",
         | 
| 710 | 
            +
                                "file_path": file_path,
         | 
| 711 | 
            +
                                "suggestions": suggestions
         | 
| 712 | 
            +
                            }
         | 
| 713 | 
            +
                            
         | 
| 714 | 
            +
                        finally:
         | 
| 715 | 
            +
                            os.chdir(original_cwd)
         | 
| 716 | 
            +
                            
         | 
| 717 | 
            +
                    except Exception as e:
         | 
| 718 | 
            +
                        self.logger.error(f"Error in generate_git_diff: {e}")
         | 
| 719 | 
            +
                        return {
         | 
| 720 | 
            +
                            "success": False,
         | 
| 721 | 
            +
                            "error": f"Git diff generation failed: {str(e)}",
         | 
| 722 | 
            +
                            "file_path": file_path
         | 
| 723 | 
            +
                        }
         |