├── .env.example ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SHOWCASE.md ├── __init__.py ├── analyzers.py ├── code_review.py ├── config.example.json ├── config.py ├── content_generator.py ├── focus.py ├── install.sh ├── project_detector.py ├── requirements.txt ├── rules_analyzer.py ├── rules_generator.py ├── run.sh ├── setup.py ├── templates └── default.cursorrules.json └── ~ └── Library └── LaunchAgents └── com.cursorfocus.plist /.env.example: -------------------------------------------------------------------------------- 1 | # Get your API key from: https://makersuite.google.com/app/apikey 2 | GEMINI_API_KEY=your_api_key_here 3 | 4 | # Optional configuration 5 | UPDATE_INTERVAL=60 # Update interval in seconds for monitoring 6 | 7 | PROJECT_PATH=. # Default to current directory -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Virtual Environment 27 | venv/ 28 | ENV/ 29 | env/ 30 | 31 | # IDE 32 | .idea/ 33 | .vscode/ 34 | *.swp 35 | *.swo 36 | .DS_Store 37 | 38 | # Generated files 39 | CodeReview.md 40 | Focus.md 41 | .cursorrules 42 | 43 | # Logs 44 | *.log 45 | logs/ 46 | 47 | # CursorFocus specific 48 | config.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.0.0] - 2024-01-19 6 | 7 | ### Added 8 | - AI-powered code review functionality using Gemini API 9 | - Intelligent file and function documentation 10 | - Duplicate code detection 11 | - Security analysis features 12 | - Project structure analysis 13 | - Automated .cursorrules generation 14 | - Real-time project monitoring 15 | 16 | ### Changed 17 | - Improved documentation with clear setup instructions 18 | - Enhanced error handling and API usage 19 | - Restructured project for better maintainability 20 | 21 | ### Fixed 22 | - API key handling and security 23 | - File path handling across different OS 24 | - Generated file formatting 25 | 26 | ## [0.1.0] - 2024-01-18 27 | 28 | ### Added 29 | - Initial release 30 | - Basic project structure monitoring 31 | - File change detection 32 | - Simple documentation generation -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CursorFocus 2 | 3 | We love your input! We want to make contributing to CursorFocus as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## Development Process 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | 1. Fork the repo and create your branch from `main` 16 | 2. If you've added code that should be tested, add tests 17 | 3. If you've changed APIs, update the documentation 18 | 4. Ensure the test suite passes 19 | 5. Make sure your code lints 20 | 6. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using GitHub's [issue tracker] 27 | 28 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! 29 | 30 | ## Write bug reports with detail, background, and sample code 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | ## Use a Consistent Coding Style 43 | 44 | * 4 spaces for indentation rather than tabs 45 | * You can try running `pylint` for style unification 46 | * Keep functions focused and single-purpose 47 | * Document complex logic 48 | 49 | ## License 50 | 51 | By contributing, you agree that your contributions will be licensed under its MIT License. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dror Bengal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CursorFocus 2 | 3 | An AI-powered code review and project analysis tool that provides intelligent, contextual descriptions of your codebase. 4 | 5 | ## Features 6 | 7 | - 🔄 Automated Code Reviews with AI-powered insights 8 | - 📝 Intelligent file and function documentation 9 | - 🌳 Project structure analysis and visualization 10 | - 📏 Code quality metrics and alerts 11 | - 🎯 Smart project type detection 12 | - 🔍 Duplicate code detection 13 | - 🧩 Modular and extensible design 14 | - 🎛️ Customizable rules and configurations 15 | - 🔄 Real-time project monitoring 16 | 17 | ## Installation 18 | 19 | 1. Clone the repository: 20 | ```bash 21 | git clone https://github.com/Dror-Bengal/CursorFocus.git 22 | ``` 23 | 24 | 2. Create and activate a virtual environment (recommended): 25 | ```bash 26 | python3 -m venv venv 27 | source venv/bin/activate # On Windows: venv\Scripts\activate 28 | ``` 29 | 30 | 3. Install the package in development mode: 31 | ```bash 32 | cd CursorFocus 33 | pip install -e . 34 | ``` 35 | 36 | 4. Create a `.env` file from the template: 37 | ```bash 38 | cp .env.example .env 39 | ``` 40 | 41 | 5. Add your Gemini API key to the `.env` file: 42 | ``` 43 | GEMINI_API_KEY=your_api_key_here 44 | ``` 45 | 46 | ## Usage 47 | 48 | ### Generate a Code Review 49 | 50 | From your project directory: 51 | 52 | ```bash 53 | cursorfocus-review 54 | ``` 55 | 56 | This will generate a `CodeReview.md` file in your project root with: 57 | - Project structure analysis 58 | - File-by-file review 59 | - Function documentation 60 | - Code duplication alerts 61 | - Project metrics 62 | 63 | ### Monitor Project Changes 64 | 65 | To start real-time project monitoring: 66 | 67 | ```bash 68 | cursorfocus 69 | ``` 70 | 71 | This will create and update a `Focus.md` file in your project root with: 72 | - Current project state 73 | - Directory structure 74 | - File analysis 75 | - Development guidelines 76 | 77 | ## Configuration 78 | 79 | You can customize CursorFocus by creating a `config.json` file in your project root: 80 | 81 | ```json 82 | { 83 | "ignored_directories": [ 84 | "node_modules", 85 | "venv", 86 | ".git" 87 | ], 88 | "ignored_files": [ 89 | "*.pyc", 90 | ".DS_Store" 91 | ], 92 | "max_depth": 3, 93 | "update_interval": 60 94 | } 95 | ``` 96 | 97 | ## Requirements 98 | 99 | - Python 3.8 or higher 100 | - Google Gemini API key 101 | 102 | ## Contributing 103 | 104 | See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project. 105 | 106 | ## License 107 | 108 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 109 | 110 | ## Changelog 111 | 112 | See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history. -------------------------------------------------------------------------------- /SHOWCASE.md: -------------------------------------------------------------------------------- 1 | # CursorFocus Showcase 2 | 3 | This showcase demonstrates the key features and capabilities of CursorFocus using real examples. 4 | 5 | ## Example Output 6 | 7 | Below is an example of a Focus.md file generated for a React project: 8 | 9 | ```markdown 10 | # Project Focus: E-commerce Dashboard 11 | 12 | **Current Goal:** Build a modern e-commerce dashboard with real-time analytics and inventory management. 13 | 14 | **Key Components:** 15 | 📁 src/ 16 | ├── components/ 17 | │ ├── Dashboard/ 18 | │ ├── Analytics/ 19 | │ └── Inventory/ 20 | ├── services/ 21 | │ └── api/ 22 | └── utils/ 23 | 24 | **Project Context:** 25 | Type: React Application 26 | Target Users: E-commerce store managers and administrators 27 | Main Functionality: Real-time sales tracking and inventory management 28 | 29 | Key Requirements: 30 | - Real-time sales dashboard 31 | - Inventory management system 32 | - Analytics visualization 33 | - User authentication 34 | 35 | **Development Guidelines:** 36 | - Keep code modular and reusable 37 | - Follow React best practices 38 | - Maintain clean separation of concerns 39 | 40 | # File Analysis 41 | 42 | `src/components/Dashboard/SalesOverview.tsx` (280 lines) 43 | **Main Responsibilities:** Main dashboard component displaying sales metrics and KPIs 44 | **Key Functions:** 45 | : Custom hook for fetching and processing real-time sales data from the API 46 | : Renders an interactive chart showing daily/monthly sales trends with customizable date ranges 47 | : Displays key performance indicators in a responsive grid layout with real-time updates 48 | 49 | `src/services/api/salesApi.ts` (180 lines) 50 | **Main Responsibilities:** API service for sales-related operations 51 | **Key Functions:** 52 | : Retrieves sales data with support for filtering and date ranges 53 | : Processes raw sales data to calculate various business metrics 54 | : Aggregates sales data by day, week, or month for trend analysis 55 | 56 | `src/components/Inventory/ProductList.tsx` (320 lines) 57 | **Main Responsibilities:** Product inventory management interface 58 | **Key Functions:** 59 | : Manages inventory data state and operations 60 | : Renders a sortable and filterable table of products 61 | : Displays alerts for low stock items and inventory issues 62 | **📄 Long-file Alert: File exceeds the recommended 250 lines for .tsx files (320 lines)** 63 | 64 | Last updated: December 28, 2023 at 11:45 PM 65 | ``` 66 | 67 | ## Feature Highlights 68 | 69 | ### 1. Smart Project Type Detection 70 | CursorFocus automatically detects your project type and provides relevant information: 71 | - React/Node.js projects: Component structure, hooks, and API endpoints 72 | - Python projects: Classes, functions, and module relationships 73 | - Chrome Extensions: Manifest details and extension components 74 | 75 | ### 2. File Length Standards 76 | The tool provides customized alerts based on file type: 77 | ```markdown 78 | **📄 Long-file Alert: File exceeds the recommended 250 lines for .tsx files (320 lines)** 79 | ``` 80 | 81 | ### 3. Detailed Function Analysis 82 | Functions are analyzed with context-aware descriptions: 83 | ```markdown 84 | : Custom hook for fetching and processing real-time sales data from the API 85 | : Renders a sortable and filterable table of products with pagination 86 | ``` 87 | 88 | ### 4. Directory Visualization 89 | Clear, hierarchical representation of your project structure: 90 | ```markdown 91 | 📁 src/ 92 | ├── components/ 93 | │ ├── Dashboard/ 94 | │ ├── Analytics/ 95 | │ └── Inventory/ 96 | ├── services/ 97 | │ └── api/ 98 | └── utils/ 99 | ``` 100 | 101 | ### 5. Project Context 102 | Comprehensive project overview with key information: 103 | ```markdown 104 | Type: React Application 105 | Target Users: E-commerce store managers 106 | Main Functionality: Real-time sales tracking 107 | ``` 108 | 109 | ## Real-World Use Cases 110 | 111 | ### 1. Onboarding New Developers 112 | - Quick project overview and structure understanding 113 | - Identification of key components and their responsibilities 114 | - Clear view of coding standards and file organization 115 | 116 | ### 2. Code Review and Maintenance 117 | - File length monitoring for maintainability 118 | - Function documentation for better understanding 119 | - Project structure visualization for navigation 120 | 121 | ### 3. Technical Documentation 122 | - Automated documentation generation 123 | - Real-time updates as code changes 124 | - Consistent format across projects 125 | 126 | ### 4. Project Management 127 | - Progress tracking through file and function analysis 128 | - Code organization oversight 129 | - Standards compliance monitoring 130 | 131 | ## Tips for Best Results 132 | 133 | 1. **File Organization:** 134 | - Keep related files in appropriate directories 135 | - Use meaningful file names 136 | - Maintain a clean project structure 137 | 138 | 2. **Function Documentation:** 139 | - Write clear function names 140 | - Add descriptive comments 141 | - Follow consistent documentation patterns 142 | 143 | 3. **Configuration:** 144 | - Customize ignored directories for your needs 145 | - Adjust file length standards if needed 146 | - Set appropriate scan depth for your project 147 | 148 | 4. **Regular Updates:** 149 | - Keep CursorFocus running for real-time updates 150 | - Review the Focus.md file periodically 151 | - Use alerts to maintain code quality -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CursorFocus - AI-powered code review and project analysis tool 3 | """ 4 | 5 | __version__ = "1.0.0" 6 | __author__ = "Dror Bengal" 7 | 8 | from .code_review import CodeReviewGenerator 9 | from .focus import Focus 10 | from .analyzers import CodeAnalyzer 11 | from .project_detector import ProjectDetector 12 | 13 | __all__ = [ 14 | 'CodeReviewGenerator', 15 | 'Focus', 16 | 'CodeAnalyzer', 17 | 'ProjectDetector' 18 | ] -------------------------------------------------------------------------------- /analyzers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from config import ( 4 | BINARY_EXTENSIONS, 5 | IGNORED_NAMES, 6 | NON_CODE_EXTENSIONS, 7 | CODE_EXTENSIONS, 8 | FUNCTION_PATTERNS, 9 | IGNORED_KEYWORDS 10 | ) 11 | import logging 12 | 13 | def get_combined_pattern(): 14 | """Combine all function patterns into a single regex pattern.""" 15 | return '|'.join(f'(?:{pattern})' for pattern in FUNCTION_PATTERNS.values()) 16 | 17 | def is_binary_file(filename): 18 | """Check if a file is binary or non-code based on its extension.""" 19 | ext = os.path.splitext(filename)[1].lower() 20 | 21 | # Binary extensions 22 | if ext in BINARY_EXTENSIONS: 23 | return True 24 | 25 | # Documentation and text files that shouldn't be analyzed for functions 26 | return ext in NON_CODE_EXTENSIONS 27 | 28 | def should_ignore_file(name): 29 | """Check if a file or directory should be ignored.""" 30 | return name in IGNORED_NAMES or name.startswith('.') 31 | 32 | def find_duplicate_functions(content, filename): 33 | """Find duplicate functions in a file and their line numbers.""" 34 | duplicates = {} 35 | function_lines = {} 36 | 37 | # Combined pattern for all function types 38 | combined_pattern = get_combined_pattern() 39 | 40 | # Find all function declarations 41 | for i, line in enumerate(content.split('\n'), 1): 42 | matches = re.finditer(combined_pattern, line) 43 | for match in matches: 44 | # Get the first non-None group (the function name) 45 | func_name = next(filter(None, match.groups()), None) 46 | if func_name and func_name.lower() not in IGNORED_KEYWORDS: 47 | if func_name not in function_lines: 48 | function_lines[func_name] = [] 49 | function_lines[func_name].append(i) 50 | 51 | # Identify duplicates with simplified line reporting 52 | for func_name, lines in function_lines.items(): 53 | if len(lines) > 1: 54 | # Only store first occurrence and count 55 | duplicates[func_name] = (lines[0], len(lines)) 56 | 57 | return duplicates 58 | 59 | def parse_comments(content_lines, start_index=0): 60 | """Parse both multi-line and single-line comments from a list of content lines. 61 | 62 | Args: 63 | content_lines: List of content lines to parse 64 | start_index: Starting index to parse from (default: 0) 65 | 66 | Returns: 67 | list: List of cleaned comment lines 68 | """ 69 | description = [] 70 | in_comment_block = False 71 | 72 | for line in reversed(content_lines[max(0, start_index):]): 73 | line = line.strip() 74 | 75 | # Handle JSDoc style comments 76 | if line.startswith('/**'): 77 | in_comment_block = True 78 | continue 79 | elif line.startswith('*/'): 80 | continue 81 | elif in_comment_block and line.startswith('*'): 82 | cleaned_line = line.lstrip('* ').strip() 83 | if cleaned_line and not cleaned_line.startswith('@'): 84 | description.insert(0, cleaned_line) 85 | # Handle single line comments 86 | elif line.startswith('//'): 87 | cleaned_line = line.lstrip('/ ').strip() 88 | if cleaned_line: 89 | description.insert(0, cleaned_line) 90 | # Stop if we hit code 91 | elif line and not line.startswith('/*') and not in_comment_block: 92 | break 93 | 94 | return description 95 | 96 | def extract_function_context(content, start_pos, end_pos=None): 97 | """Extract and analyze the function's content to generate a meaningful description. 98 | 99 | Args: 100 | content: Full file content 101 | start_pos: Starting position of the function 102 | end_pos: Optional ending position of the function 103 | 104 | Returns: 105 | str: A user-friendly description of the function 106 | """ 107 | # Get more context before and after the function 108 | context_before = content[max(0, start_pos-1000):start_pos].strip() 109 | 110 | # Get the next 1000 characters after function declaration to analyze 111 | context_length = 1000 if end_pos is None else end_pos - start_pos 112 | context = content[start_pos:start_pos + context_length] 113 | 114 | # Try to find function body between first { and matching } 115 | body_start = context.find('{') 116 | if body_start != -1: 117 | bracket_count = 1 118 | body_end = body_start + 1 119 | while bracket_count > 0 and body_end < len(context): 120 | if context[body_end] == '{': 121 | bracket_count += 1 122 | elif context[body_end] == '}': 123 | bracket_count -= 1 124 | body_end += 1 125 | function_body = context[body_start:body_end].strip('{}') 126 | else: 127 | # For arrow functions or other formats 128 | function_body = context.split('\n')[0] 129 | 130 | # Extract parameters with their types/descriptions 131 | params_match = re.search(r'\((.*?)\)', context) 132 | parameters = [] 133 | param_descriptions = {} 134 | if params_match: 135 | params = params_match.group(1).split(',') 136 | for param in params: 137 | param = param.strip() 138 | if param: 139 | # Look for JSDoc param descriptions in context before 140 | param_name = param.split(':')[0].strip().split('=')[0].strip() 141 | param_desc_match = re.search(rf'@param\s+{{\w+}}\s+{param_name}\s+-?\s*([^\n]+)', context_before) 142 | if param_desc_match: 143 | param_descriptions[param_name] = param_desc_match.group(1).strip() 144 | # Make parameter names readable 145 | readable_param = re.sub(r'([A-Z])', r' \1', param_name).lower() 146 | readable_param = readable_param.replace('_', ' ') 147 | parameters.append(readable_param) 148 | 149 | # Look for return value and its description 150 | return_matches = re.findall(r'return\s+([^;]+)', function_body) 151 | return_info = [] 152 | return_desc_match = re.search(r'@returns?\s+{[^}]+}\s+([^\n]+)', context_before) 153 | if return_desc_match: 154 | return_info.append(return_desc_match.group(1).strip()) 155 | elif return_matches: 156 | for ret in return_matches: 157 | ret = ret.strip() 158 | if ret and not ret.startswith('{') and len(ret) < 50: 159 | return_info.append(ret) 160 | 161 | # Look for constants or enums being used 162 | const_matches = re.findall(r'(?:const|enum)\s+(\w+)\s*=\s*{([^}]+)}', context_before) 163 | constants = {} 164 | for const_name, const_values in const_matches: 165 | values = re.findall(r'(\w+):\s*([^,]+)', const_values) 166 | if values: 167 | constants[const_name] = values 168 | 169 | # Analyze the actual purpose of the function 170 | purpose = [] 171 | 172 | # Check for validation logic 173 | if re.search(r'(valid|invalid|check|verify|test)\w*', function_body, re.I): 174 | conditions = [] 175 | # Look for specific conditions being checked 176 | condition_matches = re.findall(r'if\s*\((.*?)\)', function_body) 177 | for cond in condition_matches[:2]: # Get first two conditions 178 | cond = cond.strip() 179 | if len(cond) < 50 and '&&' not in cond and '||' not in cond: 180 | conditions.append(cond.replace('!', 'not ')) 181 | if conditions: 182 | purpose.append(f"validates {' and '.join(conditions)}") 183 | else: 184 | purpose.append("validates input") 185 | 186 | # Check for scoring/calculation logic with tiers 187 | if re.search(r'TIER_\d+|score|calculate|compute', function_body, re.I): 188 | # Look for tier assignments 189 | tier_matches = re.findall(r'return\s+(\w+)\.TIER_(\d+)', function_body) 190 | if tier_matches: 191 | tiers = [f"Tier {tier}" for _, tier in tier_matches] 192 | if constants and 'TIER_SCORES' in constants: 193 | tier_info = [] 194 | for tier_name, tier_score in constants['TIER_SCORES']: 195 | if any(t in tier_name for t in tiers): 196 | tier_info.append(f"{tier_name.lower()}: {tier_score}") 197 | if tier_info: 198 | purpose.append(f"assigns scores ({', '.join(tier_info)})") 199 | else: 200 | purpose.append(f"assigns {' or '.join(tiers)} scores") 201 | else: 202 | # Look for other score calculations 203 | calc_matches = re.findall(r'(\w+(?:Score|Rating|Value))\s*[+\-*/]=\s*([^;]+)', function_body) 204 | if calc_matches: 205 | calc_vars = [match[0] for match in calc_matches if len(match[0]) < 30] 206 | if calc_vars: 207 | purpose.append(f"calculates {' and '.join(calc_vars)}") 208 | 209 | # Check for store validation 210 | if re.search(r'store|domain|source', function_body, re.I): 211 | store_checks = [] 212 | # Look for store list checks 213 | if 'STORE_CATEGORIES' in constants: 214 | store_types = [store[0] for store in constants['STORE_CATEGORIES']] 215 | if store_types: 216 | store_checks.append(f"checks against {', '.join(store_types)}") 217 | # Look for domain validation 218 | domain_checks = re.findall(r'\.(includes|match(?:es)?)\(([^)]+)\)', function_body) 219 | if domain_checks: 220 | store_checks.append("validates domain format") 221 | if store_checks: 222 | purpose.append(" and ".join(store_checks)) 223 | 224 | # Check for data transformation 225 | if re.search(r'(map|filter|reduce|transform|convert|parse|format|normalize)', function_body, re.I): 226 | transform_matches = re.findall(r'(\w+)\s*\.\s*(map|filter|reduce)', function_body) 227 | if transform_matches: 228 | items = [match[0] for match in transform_matches if len(match[0]) < 20] 229 | if items: 230 | purpose.append(f"processes {' and '.join(items)}") 231 | 232 | # Look for specific number ranges and their context 233 | range_matches = re.findall(r'([<>]=?)\s*(\d+)', function_body) 234 | ranges = [] 235 | for op, num in range_matches: 236 | # Look for variable name or context before comparison 237 | context_match = re.search(rf'\b(\w+)\s*{op}\s*{num}', function_body) 238 | if context_match: 239 | var_name = context_match.group(1) 240 | var_name = re.sub(r'([A-Z])', r' \1', var_name).lower() 241 | ranges.append(f"{var_name} {op} {num}") 242 | 243 | # Generate a user-friendly description 244 | description_parts = [] 245 | 246 | # Add main purpose if found 247 | if purpose: 248 | description_parts.append(f"This function {' and '.join(purpose)}") 249 | 250 | # Add parameter descriptions if available 251 | if param_descriptions: 252 | desc = [] 253 | for param, description in param_descriptions.items(): 254 | if len(description) < 50: # Keep only concise descriptions 255 | desc.append(f"{param}: {description}") 256 | if desc: 257 | description_parts.append(f"Takes {', '.join(desc)}") 258 | elif parameters: 259 | description_parts.append(f"Takes {' and '.join(parameters)}") 260 | 261 | # Add range information if found 262 | if ranges: 263 | description_parts.append(f"Ensures {' and '.join(ranges)}") 264 | 265 | # Add return description if available 266 | if return_info: 267 | description_parts.append(f"Returns {return_info[0]}") 268 | 269 | # If we couldn't generate a good description, return a simple one 270 | if not description_parts: 271 | return "This function helps with the program's functionality" 272 | 273 | return " | ".join(description_parts) 274 | 275 | def analyze_file_content(file_path): 276 | """Analyze file content for functions and their descriptions.""" 277 | try: 278 | # Skip binary and non-code files 279 | if is_binary_file(file_path): 280 | return [], 0 281 | 282 | with open(file_path, 'r', encoding='utf-8') as f: 283 | content = f.read() 284 | 285 | # Skip files that don't look like actual code files 286 | ext = os.path.splitext(file_path)[1].lower() 287 | if ext not in CODE_EXTENSIONS: 288 | return [], 0 289 | 290 | functions = [] 291 | duplicates = find_duplicate_functions(content, file_path) 292 | 293 | # Use combined pattern for function detection 294 | combined_pattern = get_combined_pattern() 295 | 296 | matches = re.finditer(combined_pattern, content, re.MULTILINE | re.DOTALL) 297 | for match in matches: 298 | func_name = next(filter(None, match.groups()), None) 299 | if not func_name or func_name.lower() in IGNORED_KEYWORDS: 300 | continue 301 | 302 | # Get comment block before function 303 | start = match.start() 304 | comment_block = content[:start].strip().split('\n')[-10:] # Get up to 10 lines before function 305 | description = parse_comments(comment_block) 306 | 307 | # If no comment found or comment is too generic, analyze function content 308 | if not description or len(description[0].split()) < 5: 309 | # Extract detailed context from function body 310 | context_description = extract_function_context(content, start) 311 | 312 | # Analyze function name parts for additional context 313 | name_parts = re.findall('[A-Z][a-z]*|[a-z]+', func_name) 314 | verb = name_parts[0].lower() if name_parts else '' 315 | subject = ' '.join(name_parts[1:]).lower() if len(name_parts) > 1 else '' 316 | 317 | # Combine name analysis with context analysis 318 | if verb in ['is', 'has', 'should', 'can', 'will']: 319 | description = [f"Validates if {subject} meets criteria | {context_description}"] 320 | elif verb in ['get', 'fetch', 'retrieve']: 321 | description = [f"Retrieves {subject} data | {context_description}"] 322 | elif verb in ['set', 'update', 'modify']: 323 | description = [f"Updates {subject} | {context_description}"] 324 | elif verb in ['calc', 'compute', 'calculate']: 325 | description = [f"Calculates {subject} | {context_description}"] 326 | elif verb in ['handle', 'process']: 327 | description = [f"Processes {subject} | {context_description}"] 328 | elif verb in ['validate', 'verify']: 329 | description = [f"Validates {subject} | {context_description}"] 330 | elif verb in ['create', 'init', 'initialize']: 331 | description = [f"Creates {subject} | {context_description}"] 332 | elif verb in ['sort', 'order']: 333 | description = [f"Sorts {subject} | {context_description}"] 334 | else: 335 | description = [context_description] 336 | 337 | final_description = ' '.join(description) 338 | 339 | # Add duplicate alert if needed, now with simplified line reporting 340 | if func_name in duplicates: 341 | first_line, count = duplicates[func_name] 342 | final_description += f" **🔄 Duplicate Alert: Function appears {count} times (first occurrence: line {first_line})**" 343 | 344 | functions.append((func_name, final_description)) 345 | 346 | return functions, len(content.split('\n')) 347 | except Exception as e: 348 | print(f"Error analyzing file {file_path}: {e}") 349 | return [], 0 350 | 351 | class RulesAnalyzer: 352 | def __init__(self, project_path): 353 | self.project_path = project_path 354 | 355 | def analyze_project_for_rules(self): 356 | """Analyze project for .cursorrules generation""" 357 | try: 358 | project_info = { 359 | "name": self.detect_project_name(), 360 | "version": self.detect_version(), 361 | "language": self.detect_main_language(), 362 | "framework": self.detect_framework(), 363 | "type": self.determine_project_type() 364 | } 365 | return project_info 366 | except Exception as e: 367 | logging.error(f"Error analyzing project for rules: {e}") 368 | return self.get_default_project_info() -------------------------------------------------------------------------------- /code_review.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import datetime 4 | from pathlib import Path 5 | from typing import Dict, List, Optional, Any 6 | import re 7 | from collections import defaultdict 8 | import time 9 | from difflib import SequenceMatcher 10 | 11 | class CodeReviewGenerator: 12 | def __init__(self, api_key: str): 13 | self.api_key = api_key 14 | 15 | def read_file_content(self, file_path: str) -> str: 16 | """Read content of a specific file""" 17 | try: 18 | with open(file_path, 'r') as f: 19 | return f.read() 20 | except Exception as e: 21 | logging.error(f"Error reading file {file_path}: {str(e)}") 22 | return "" 23 | 24 | def get_relevant_files(self, project_path: str) -> List[str]: 25 | """Get list of relevant files for review""" 26 | ignored = {'.git', 'node_modules', '.next', 'dist', 'build', 'coverage'} 27 | files = [] 28 | 29 | for root, dirs, filenames in os.walk(project_path): 30 | # Skip ignored directories 31 | dirs[:] = [d for d in dirs if d not in ignored] 32 | 33 | # Skip CursorFocus directory 34 | if 'CursorFocus' in root.split(os.sep): 35 | continue 36 | 37 | for filename in filenames: 38 | if filename.endswith(('.js', '.jsx', '.ts', '.tsx', '.py', '.css', '.scss')): 39 | file_path = os.path.join(root, filename) 40 | files.append(file_path) 41 | return files 42 | 43 | def analyze_code_structure(self, project_path: str, files: List[str]) -> Dict: 44 | """Analyze code structure and organization""" 45 | structure_analysis = { 46 | 'technical': [], 47 | 'simple': [], 48 | 'files_by_type': defaultdict(list), 49 | 'potential_unused': [] 50 | } 51 | 52 | # Analyze file organization 53 | for file in files: 54 | ext = os.path.splitext(file)[1] 55 | rel_path = os.path.relpath(file, project_path) 56 | structure_analysis['files_by_type'][ext].append(rel_path) 57 | 58 | # Check for potentially unused files 59 | if os.path.getsize(file) < 50: # Small files might be unused 60 | structure_analysis['potential_unused'].append(rel_path) 61 | 62 | return structure_analysis 63 | 64 | def analyze_coding_standards(self, file_contents: Dict[str, str]) -> Dict: 65 | """Analyze coding standards and style""" 66 | standards_analysis = { 67 | 'technical': [], 68 | 'simple': [], 69 | 'style_issues': defaultdict(list) 70 | } 71 | 72 | for file_path, content in file_contents.items(): 73 | lines = content.split('\n') 74 | for i, line in enumerate(lines, 1): 75 | # Check indentation 76 | if line.strip() and line[0] != ' ' and not line.startswith(('import', 'from', 'class', 'def')): 77 | standards_analysis['style_issues']['indentation'].append((file_path, i)) 78 | 79 | # Check line length 80 | if len(line) > 100: 81 | standards_analysis['style_issues']['line_length'].append((file_path, i)) 82 | 83 | # Check naming conventions 84 | if re.search(r'[a-z][A-Z]', line): # Detect mixed case 85 | standards_analysis['style_issues']['naming'].append((file_path, i)) 86 | 87 | return standards_analysis 88 | 89 | def find_duplicate_code(self, file_contents: Dict[str, str]) -> Dict: 90 | """Detect duplicate code blocks""" 91 | duplicates = { 92 | 'technical': [], 93 | 'simple': [], 94 | 'duplicates': [] 95 | } 96 | 97 | # Simple duplicate function detection 98 | function_patterns = { 99 | 'py': r'def\s+(\w+)', 100 | 'js': r'function\s+(\w+)|const\s+(\w+)\s*=\s*\(', 101 | 'ts': r'function\s+(\w+)|const\s+(\w+)\s*=\s*\(' 102 | } 103 | 104 | functions = defaultdict(list) 105 | for file_path, content in file_contents.items(): 106 | ext = os.path.splitext(file_path)[1][1:] 107 | pattern = function_patterns.get(ext) 108 | if pattern: 109 | matches = re.finditer(pattern, content) 110 | for match in matches: 111 | func_name = match.group(1) or match.group(2) 112 | functions[func_name].append(file_path) 113 | 114 | for func_name, files in functions.items(): 115 | if len(files) > 1: 116 | duplicates['duplicates'].append((func_name, files)) 117 | 118 | return duplicates 119 | 120 | def check_security_issues(self, file_contents: Dict[str, str]) -> Dict: 121 | """Check for security issues""" 122 | security_analysis = { 123 | 'technical': [], 124 | 'simple': [], 125 | 'issues': [] 126 | } 127 | 128 | # Check for hardcoded secrets 129 | secret_patterns = [ 130 | r'api[_-]key\s*=\s*["\']([^"\']+)["\']', 131 | r'password\s*=\s*["\']([^"\']+)["\']', 132 | r'secret\s*=\s*["\']([^"\']+)["\']', 133 | r'token\s*=\s*["\']([^"\']+)["\']' 134 | ] 135 | 136 | for file_path, content in file_contents.items(): 137 | for pattern in secret_patterns: 138 | matches = re.finditer(pattern, content, re.IGNORECASE) 139 | for match in matches: 140 | security_analysis['issues'].append({ 141 | 'file': file_path, 142 | 'type': 'hardcoded_secret', 143 | 'pattern': pattern 144 | }) 145 | 146 | return security_analysis 147 | 148 | def analyze_error_handling(self, file_contents: Dict[str, str]) -> Dict: 149 | """Analyze error handling practices""" 150 | error_analysis = { 151 | 'technical': [], 152 | 'simple': [], 153 | 'missing_error_handling': [] 154 | } 155 | 156 | for file_path, content in file_contents.items(): 157 | if file_path.endswith(('.py', '.js', '.ts')): 158 | # Check for functions without try-catch 159 | function_blocks = re.finditer(r'(async\s+)?(?:function|def)\s+\w+\s*\([^)]*\)\s*{?[^}]*$', content, re.MULTILINE) 160 | for match in function_blocks: 161 | block_content = content[match.start():match.end()] 162 | if 'try' not in block_content and 'catch' not in block_content: 163 | error_analysis['missing_error_handling'].append(file_path) 164 | 165 | return error_analysis 166 | 167 | def check_documentation(self, file_contents: Dict[str, str]) -> Dict: 168 | """Check documentation coverage""" 169 | doc_analysis = { 170 | 'technical': [], 171 | 'simple': [], 172 | 'missing_docs': [] 173 | } 174 | 175 | for file_path, content in file_contents.items(): 176 | if file_path.endswith('.py'): 177 | # Check for missing docstrings 178 | functions = re.finditer(r'def\s+\w+\s*\([^)]*\):', content) 179 | for match in functions: 180 | pos = match.end() 181 | next_lines = content[pos:pos+100] 182 | if '"""' not in next_lines and "'''" not in next_lines: 183 | doc_analysis['missing_docs'].append(file_path) 184 | 185 | return doc_analysis 186 | 187 | def generate_review_prompt(self, focus_content: str, analysis_results: Dict) -> str: 188 | """Generate the prompt for AI review with analysis results""" 189 | prompt = """As an expert code reviewer, analyze the following codebase and provide a comprehensive review. 190 | 191 | Project Overview from Focus.md: 192 | {focus_content} 193 | 194 | Analysis Results: 195 | {analysis_results} 196 | 197 | Please provide a detailed review in two sections: 198 | 199 | 1. Technical Review 200 | - Code structure and organization assessment 201 | - Coding standards compliance 202 | - Security vulnerabilities 203 | - Performance considerations 204 | - Error handling practices 205 | - Documentation coverage 206 | - Duplicate code analysis 207 | - Dependency management 208 | - Testing coverage 209 | 210 | 2. Simple Explanation 211 | - Overall project organization 212 | - Code clarity and maintainability 213 | - Security concerns in plain language 214 | - Suggestions for improvement 215 | - Priority action items 216 | 217 | For each section, provide specific examples and actionable recommendations. 218 | """ 219 | return prompt.format( 220 | focus_content=focus_content, 221 | analysis_results=str(analysis_results) 222 | ) 223 | 224 | def analyze_file(self, file_path: str, content: str, all_files_content: Dict[str, str]) -> Dict: 225 | """Analyze a single file and provide simple explanation""" 226 | # Store the current file path for use in _extract_functions 227 | self.current_file = file_path 228 | 229 | # Get file type 230 | file_type = os.path.splitext(file_path)[1][1:] 231 | 232 | analysis = { 233 | 'explanation': self._generate_simple_explanation(file_path, content), 234 | 'similar_files': [], 235 | 'functionality_alerts': [] 236 | } 237 | 238 | # Get line count 239 | lines = content.splitlines() 240 | line_count = len(lines) 241 | if line_count > 200: # Even lower threshold for length warnings 242 | analysis['functionality_alerts'].append({ 243 | 'type': 'length_warning', 244 | 'details': f'File exceeds recommended length of 200 lines', 245 | 'count': line_count 246 | }) 247 | 248 | # Extract and analyze functions 249 | functions = self._extract_functions(content, file_type) 250 | 251 | # Find duplicate functions with line numbers 252 | if file_type in ['js', 'jsx', 'ts', 'tsx', 'py']: 253 | function_counts = defaultdict(list) 254 | 255 | # First pass: collect all functions 256 | for other_path, other_content in all_files_content.items(): 257 | if other_path != file_path: 258 | other_functions = self._extract_functions(other_content, os.path.splitext(other_path)[1][1:]) 259 | for func in other_functions: 260 | function_counts[func['name']].append(other_path) 261 | 262 | # Second pass: check for duplicates 263 | for func in functions: 264 | if func['name'] in function_counts: 265 | # Find the line number 266 | for i, line in enumerate(lines, 1): 267 | if func['name'] in line and ('function' in line or 'def' in line or '=>' in line): 268 | analysis['functionality_alerts'].append({ 269 | 'type': 'duplicate_functionality', 270 | 'details': f"Function '{func['name']}' is duplicated", 271 | 'count': len(function_counts[func['name']]) + 1, 272 | 'line': i, 273 | 'locations': function_counts[func['name']] 274 | }) 275 | break 276 | 277 | # Find similar file content 278 | for other_path, other_content in all_files_content.items(): 279 | if other_path != file_path: 280 | similarity = self._calculate_similarity( 281 | self._clean_content(content), 282 | self._clean_content(other_content) 283 | ) 284 | if similarity > 0.7: # 70% similarity threshold 285 | analysis['functionality_alerts'].append({ 286 | 'type': 'similar_content', 287 | 'details': f"File has {int(similarity * 100)}% similar content with {os.path.basename(other_path)}", 288 | 'file': other_path 289 | }) 290 | else: 291 | # Add to related files if there's some similarity 292 | if similarity > 0.3: # 30% similarity threshold for related files 293 | analysis['similar_files'].append({ 294 | 'file': other_path 295 | }) 296 | 297 | return analysis 298 | 299 | def _are_names_similar(self, name1: str, name2: str) -> bool: 300 | """Check if two file names are similar""" 301 | # Remove extension and common prefixes/suffixes 302 | name1 = os.path.splitext(name1)[0] 303 | name2 = os.path.splitext(name2)[0] 304 | 305 | # Remove common words 306 | common_words = ['test', 'utils', 'helper', 'component', 'page', 'api'] 307 | for word in common_words: 308 | name1 = name1.replace(word, '') 309 | name2 = name2.replace(word, '') 310 | 311 | # Compare the core names 312 | return name1 and name2 and (name1 in name2 or name2 in name1) 313 | 314 | def _have_similar_content(self, content1: str, content2: str) -> bool: 315 | """Check if two files have similar content""" 316 | # Remove comments and whitespace 317 | content1 = self._clean_content(content1) 318 | content2 = self._clean_content(content2) 319 | 320 | # If either content is empty, return False 321 | if not content1 or not content2: 322 | return False 323 | 324 | # Calculate similarity ratio 325 | similarity = SequenceMatcher(None, content1, content2).ratio() 326 | return similarity > 0.7 # Files are considered similar if they're 70% identical 327 | 328 | def _clean_content(self, content: str) -> str: 329 | """Remove comments and whitespace from content""" 330 | # Remove single-line comments 331 | content = re.sub(r'//.*$', '', content, flags=re.MULTILINE) 332 | content = re.sub(r'#.*$', '', content, flags=re.MULTILINE) 333 | 334 | # Remove multi-line comments 335 | content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) 336 | 337 | # Remove empty lines and whitespace 338 | return '\n'.join(line.strip() for line in content.splitlines() if line.strip()) 339 | 340 | def _generate_function_description(self, func_name: str, func_body: str, file_type: str) -> str: 341 | """Generate a simple description of what a function does""" 342 | 343 | # Special case components 344 | special_components = { 345 | 'RootLayout': "The main layout component that wraps all pages", 346 | 'Layout': "Provides the structure and layout for pages", 347 | 'Loading': "Shows a loading state while content is being prepared", 348 | 'NotFound': "Displays a friendly 404 error page when content isn't found", 349 | 'Error': "Shows an error message when something goes wrong", 350 | 'Page': "The main content component for this route", 351 | 'Header': "The top section of the page with navigation and branding", 352 | 'Footer': "The bottom section of the page with additional links and info", 353 | 'Sidebar': "A side panel with navigation or additional content", 354 | 'Navigation': "Helps users move between different parts of the site", 355 | 'Modal': "A popup window that appears over the main content", 356 | 'Dialog': "A popup window for user interactions or messages", 357 | 'Form': "Collects user input through various fields", 358 | 'Button': "A clickable element that triggers actions", 359 | 'Input': "Allows users to enter text or data", 360 | 'Select': "Lets users choose from a list of options", 361 | 'Card': "Displays content in a card-style container", 362 | 'List': "Shows multiple items in a structured way", 363 | 'Table': "Displays data in rows and columns", 364 | 'Menu': "Shows a list of options or actions", 365 | 'Dropdown': "Reveals additional options when clicked", 366 | 'Tabs': "Organizes content into different sections", 367 | 'Alert': "Shows important messages or notifications", 368 | 'Toast': "Displays temporary notification messages", 369 | 'Tooltip': "Shows helpful text when hovering over elements", 370 | 'Badge': "Displays a small count or status indicator", 371 | 'Avatar': "Shows a user's profile picture or icon", 372 | 'Icon': "Displays a small symbolic image", 373 | 'Spinner': "Shows an animated loading indicator", 374 | 'Progress': "Indicates progress of an operation", 375 | 'Skeleton': "Shows a placeholder while content loads" 376 | } 377 | 378 | # Check for special case components first 379 | if func_name in special_components: 380 | return special_components[func_name] 381 | 382 | # Handle Next.js special files 383 | if hasattr(self, 'current_file'): 384 | file_name = os.path.basename(self.current_file) 385 | base_name = os.path.splitext(file_name)[0] 386 | 387 | # Map file names to component descriptions 388 | next_components = { 389 | 'layout': ('RootLayout', "The main layout component that wraps all pages"), 390 | 'page': ('Page', "The main content component for this route"), 391 | 'loading': ('Loading', "Shows a loading state while content is being prepared"), 392 | 'not-found': ('NotFound', "Displays a friendly 404 error page when content isn't found"), 393 | 'error': ('Error', "Shows an error message when something goes wrong"), 394 | 'middleware': ('middleware', "Handles request middleware for authentication and routing") 395 | } 396 | 397 | if base_name in next_components: 398 | component_name, description = next_components[base_name] 399 | return description 400 | 401 | # Extract parameters if they exist 402 | params = re.findall(r'\((.*?)\)', func_body.split('\n')[0]) 403 | param_list = [] 404 | if params: 405 | # Clean up parameters 406 | param_text = params[0].strip() 407 | if param_text and param_text != '()': 408 | param_list = [p.strip().split(':')[0] for p in param_text.split(',')] 409 | 410 | # Check if it's a React component 411 | if file_type in ['tsx', 'jsx'] and func_name[0].isupper(): 412 | # Look for common UI patterns in the name 413 | component_types = { 414 | 'button': "a clickable button", 415 | 'list': "a list of items", 416 | 'form': "a form for user input", 417 | 'modal': "a popup window", 418 | 'dialog': "a popup window", 419 | 'card': "a card-style container", 420 | 'header': "header content", 421 | 'footer': "footer content", 422 | 'nav': "navigation elements", 423 | 'menu': "a menu of options", 424 | 'input': "an input field", 425 | 'select': "a dropdown selection", 426 | 'table': "a data table", 427 | 'grid': "a grid layout", 428 | 'container': "a content container", 429 | 'wrapper': "a wrapper component", 430 | 'provider': "provides data or functionality", 431 | 'view': "a view component", 432 | 'panel': "a panel of content", 433 | 'section': "a section of content" 434 | } 435 | 436 | description = "A component that shows " 437 | found_type = False 438 | 439 | # Check component name against known types 440 | for type_key, type_desc in component_types.items(): 441 | if type_key in func_name.lower(): 442 | description += type_desc 443 | found_type = True 444 | break 445 | 446 | if not found_type: 447 | # Convert PascalCase to spaces for a readable name 448 | readable_name = ' '.join(re.findall('[A-Z][^A-Z]*', func_name)).lower() 449 | description += f"{readable_name}" 450 | 451 | # Add parameter context if available 452 | if param_list: 453 | description += f" (uses: {', '.join(param_list)})" 454 | return description 455 | 456 | # Common React/Next.js patterns 457 | react_patterns = { 458 | r'^use[A-Z]': "A custom hook that ", 459 | r'^handle[A-Z]': "Handles when ", 460 | r'^on[A-Z]': "Responds when ", 461 | r'^get[A-Z]': "Gets or retrieves ", 462 | r'^set[A-Z]': "Updates or changes ", 463 | r'^is[A-Z]': "Checks if ", 464 | r'^has[A-Z]': "Checks if there is ", 465 | r'^format[A-Z]': "Formats or arranges ", 466 | r'^validate[A-Z]': "Checks if valid ", 467 | r'^parse[A-Z]': "Processes and understands ", 468 | r'^render[A-Z]': "Shows or displays ", 469 | r'^create[A-Z]': "Creates or makes new ", 470 | r'^update[A-Z]': "Updates or modifies ", 471 | r'^delete[A-Z]': "Removes or deletes ", 472 | r'^fetch[A-Z]': "Gets data from ", 473 | r'^load[A-Z]': "Loads or prepares ", 474 | r'^save[A-Z]': "Saves or stores ", 475 | r'^convert[A-Z]': "Converts or changes ", 476 | r'^calculate[A-Z]': "Calculates or computes ", 477 | r'^filter[A-Z]': "Filters or selects ", 478 | r'^sort[A-Z]': "Arranges or orders ", 479 | r'^search[A-Z]': "Searches for ", 480 | r'^find[A-Z]': "Finds or locates ", 481 | r'^toggle[A-Z]': "Switches between ", 482 | r'^show[A-Z]': "Displays or reveals ", 483 | r'^hide[A-Z]': "Hides or removes from view ", 484 | r'^open[A-Z]': "Opens or shows ", 485 | r'^close[A-Z]': "Closes or hides ", 486 | r'^enable[A-Z]': "Turns on or activates ", 487 | r'^disable[A-Z]': "Turns off or deactivates ", 488 | r'^add[A-Z]': "Adds or includes ", 489 | r'^remove[A-Z]': "Removes or takes away ", 490 | r'^clear[A-Z]': "Clears or resets ", 491 | r'^reset[A-Z]': "Resets or restores " 492 | } 493 | 494 | # Check for common function patterns 495 | for pattern, desc_prefix in react_patterns.items(): 496 | if re.match(pattern, func_name): 497 | # Convert camelCase to spaces after the prefix 498 | name_parts = re.sub(r'([A-Z])', r' \1', func_name).split() 499 | action_part = ' '.join(name_parts[1:]).lower() 500 | description = desc_prefix + action_part 501 | 502 | # Add parameter context if available 503 | if param_list: 504 | description += f" (uses: {', '.join(param_list)})" 505 | return description 506 | 507 | # If no pattern matched, create a basic description 508 | # Convert camelCase/PascalCase to spaces 509 | readable_name = re.sub(r'([A-Z])', r' \1', func_name).lower() 510 | description = f"Handles {readable_name}" 511 | 512 | # Add parameter context if available 513 | if param_list: 514 | description += f" (uses: {', '.join(param_list)})" 515 | 516 | return description 517 | 518 | def _extract_functions(self, content: str, file_type: str) -> List[Dict[str, Any]]: 519 | """Extract functions and their details from the file content""" 520 | functions = [] 521 | 522 | # Skip binary or empty files 523 | if not content or is_binary_file(content): 524 | return functions 525 | 526 | try: 527 | # Different patterns for different file types 528 | if file_type in ('ts', 'tsx', 'js', 'jsx'): 529 | # React component pattern 530 | component_pattern = r'(?:export\s+(?:default\s+)?)?(?:const|class|function)\s+([A-Z][a-zA-Z0-9]*)\s*(?:=|\{|\()' 531 | for match in re.finditer(component_pattern, content): 532 | functions.append({ 533 | 'name': match.group(1), 534 | 'type': 'component', 535 | 'line': content.count('\n', 0, match.start()) + 1 536 | }) 537 | 538 | # Hook pattern 539 | hook_pattern = r'(?:export\s+(?:default\s+)?)?(?:const|function)\s+(use[A-Z][a-zA-Z0-9]*)\s*(?:=|\()' 540 | for match in re.finditer(hook_pattern, content): 541 | functions.append({ 542 | 'name': match.group(1), 543 | 'type': 'hook', 544 | 'line': content.count('\n', 0, match.start()) + 1 545 | }) 546 | 547 | # Regular function pattern 548 | function_pattern = r'(?:export\s+(?:default\s+)?)?(?:async\s+)?(?:function|const)\s+([a-z][a-zA-Z0-9]*)\s*(?:=|\()' 549 | for match in re.finditer(function_pattern, content): 550 | functions.append({ 551 | 'name': match.group(1), 552 | 'type': 'function', 553 | 'line': content.count('\n', 0, match.start()) + 1 554 | }) 555 | 556 | elif file_type == 'py': 557 | # Python function pattern 558 | function_pattern = r'def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(' 559 | for match in re.finditer(function_pattern, content): 560 | functions.append({ 561 | 'name': match.group(1), 562 | 'type': 'function', 563 | 'line': content.count('\n', 0, match.start()) + 1 564 | }) 565 | 566 | # Python class pattern 567 | class_pattern = r'class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\([^)]*\))?\s*:' 568 | for match in re.finditer(class_pattern, content): 569 | functions.append({ 570 | 'name': match.group(1), 571 | 'type': 'class', 572 | 'line': content.count('\n', 0, match.start()) + 1 573 | }) 574 | except Exception as e: 575 | logging.debug(f"Error extracting functions: {str(e)}") 576 | 577 | return functions 578 | 579 | def _extract_function_body(self, content: str, file_type: str) -> str: 580 | """Extract function body based on file type""" 581 | if file_type == 'py': 582 | # Python: Find indented block 583 | lines = content.splitlines() 584 | body = [] 585 | if not lines: 586 | return "" 587 | 588 | # Get first line's indentation 589 | first_line = lines[0] 590 | base_indent = len(first_line) - len(first_line.lstrip()) 591 | 592 | for line in lines: 593 | if line.strip() and len(line) - len(line.lstrip()) <= base_indent: 594 | break 595 | body.append(line) 596 | 597 | return '\n'.join(body) 598 | else: 599 | # JavaScript/TypeScript: Find block between { } 600 | stack = [] 601 | for i, char in enumerate(content): 602 | if char == '{': 603 | stack.append(i) 604 | elif char == '}': 605 | if stack: 606 | start = stack.pop() 607 | if not stack: # We found the matching outer brace 608 | return content[start:i+1] 609 | return "" 610 | 611 | def _find_duplicate_functions(self, functions1: List[Dict], functions2: List[Dict]) -> List[str]: 612 | """Find duplicate functions between two sets by comparing implementation and context""" 613 | duplicates = [] 614 | 615 | # Common method names to ignore (these are expected to appear multiple times) 616 | ignore_methods = { 617 | # React/Next.js patterns 618 | 'getLayout', 'getInitialProps', 'getStaticProps', 'getServerSideProps', 619 | 'layout', 'loading', 'error', 'notFound', 620 | # Common React hooks 621 | 'useEffect', 'useState', 'useMemo', 'useCallback', 622 | # Common utility names 623 | 'init', 'setup', 'configure', 'getConfig', 'getData', 624 | # Common class methods 625 | '__init__', '__str__', '__repr__', '__len__', 'toString', 626 | # Testing functions 627 | 'setUp', 'tearDown', 'beforeEach', 'afterEach', 628 | } 629 | 630 | for func1 in functions1: 631 | for func2 in functions2: 632 | # Skip ignored method names 633 | if func1['name'] in ignore_methods: 634 | continue 635 | 636 | # Only compare functions with same name 637 | if func1['name'] == func2['name']: 638 | # Clean and normalize the function bodies 639 | body1 = self._normalize_function_body(func1['body']) 640 | body2 = self._normalize_function_body(func2['body']) 641 | 642 | # Calculate similarity score 643 | similarity = self._calculate_similarity(body1, body2) 644 | 645 | # If bodies are very similar (80% or more), it's likely a real duplication 646 | if similarity >= 0.8: 647 | duplicates.append({ 648 | 'name': func1['name'], 649 | 'similarity': similarity, 650 | 'reason': 'Implementation is nearly identical' 651 | }) 652 | # If bodies are somewhat similar (60-80%), check the context 653 | elif similarity >= 0.6: 654 | context_similarity = self._check_function_context(func1, func2) 655 | if context_similarity >= 0.7: 656 | duplicates.append({ 657 | 'name': func1['name'], 658 | 'similarity': similarity, 659 | 'reason': 'Similar implementation and usage context' 660 | }) 661 | 662 | return duplicates 663 | 664 | def _normalize_function_body(self, body: str) -> str: 665 | """Normalize function body for comparison by removing noise""" 666 | # Remove comments 667 | body = re.sub(r'//.*$', '', body, flags=re.MULTILINE) 668 | body = re.sub(r'/\*.*?\*/', '', body, flags=re.DOTALL) 669 | body = re.sub(r'#.*$', '', body, flags=re.MULTILINE) 670 | 671 | # Remove string literals 672 | body = re.sub(r'"[^"]*"', '""', body) 673 | body = re.sub(r"'[^']*'", "''", body) 674 | 675 | # Remove whitespace and normalize line endings 676 | body = '\n'.join(line.strip() for line in body.splitlines() if line.strip()) 677 | 678 | # Remove variable names but keep structure 679 | body = re.sub(r'\b\w+\s*=', '=', body) 680 | body = re.sub(r'\bconst\s+\w+\s*=', 'const=', body) 681 | body = re.sub(r'\blet\s+\w+\s*=', 'let=', body) 682 | body = re.sub(r'\bvar\s+\w+\s*=', 'var=', body) 683 | 684 | return body 685 | 686 | def _calculate_similarity(self, text1: str, text2: str) -> float: 687 | """Calculate similarity ratio between two texts""" 688 | if not text1 or not text2: 689 | return 0.0 690 | return SequenceMatcher(None, text1, text2).ratio() 691 | 692 | def _check_function_context(self, func1: Dict, func2: Dict) -> float: 693 | """Check if functions are used in similar contexts""" 694 | # Extract function calls and dependencies 695 | calls1 = set(re.findall(r'\b\w+\(', func1['body'])) 696 | calls2 = set(re.findall(r'\b\w+\(', func2['body'])) 697 | 698 | # Extract variable usage patterns 699 | vars1 = set(re.findall(r'\b\w+\s*=|\b\w+\s*\+=|\b\w+\s*-=', func1['body'])) 700 | vars2 = set(re.findall(r'\b\w+\s*=|\b\w+\s*\+=|\b\w+\s*-=', func2['body'])) 701 | 702 | # Calculate similarity of usage patterns 703 | calls_similarity = len(calls1.intersection(calls2)) / max(len(calls1), len(calls2), 1) 704 | vars_similarity = len(vars1.intersection(vars2)) / max(len(vars1), len(vars2), 1) 705 | 706 | return (calls_similarity + vars_similarity) / 2 707 | 708 | def _generate_simple_explanation(self, file_path: str, content: str) -> str: 709 | """Generate an intelligent, context-aware explanation of what the file does""" 710 | 711 | # Get file extension and name 712 | ext = os.path.splitext(file_path)[1][1:].lower() 713 | name = os.path.basename(file_path) 714 | 715 | # Extract key information 716 | functions = self._extract_functions(content, ext) or [] 717 | imports = self._extract_imports(content) 718 | exports = self._extract_exports(content) 719 | 720 | # Initialize description components 721 | purpose = [] 722 | features = [] 723 | integrations = [] 724 | 725 | # Analyze file type and location 726 | if '/api/' in file_path: 727 | endpoint = file_path.split('/api/')[-1].split('/')[0] 728 | purpose.append(f"This API endpoint handles {endpoint} functionality") 729 | if 'route' in name.lower(): 730 | features.append("implements routing logic") 731 | 732 | elif '/components/' in file_path: 733 | component_names = [f['name'] for f in functions if f['name'][0].isupper()] 734 | if component_names: 735 | purpose.append(f"This React component implements {', '.join(component_names)}") 736 | if any(f['name'].startswith('use') for f in functions): 737 | features.append("includes custom hooks") 738 | 739 | elif '/services/' in file_path or 'service' in name.lower(): 740 | service_type = next((word for word in file_path.split('/') if 'service' in word.lower()), 'service') 741 | purpose.append(f"This service module provides {service_type.replace('Service', '').replace('service', '')} functionality") 742 | 743 | elif '/utils/' in file_path or 'util' in name.lower() or 'helper' in name.lower(): 744 | purpose.append("This utility module provides helper functions") 745 | if functions: 746 | features.append(f"includes {len(functions)} utility functions") 747 | 748 | elif '/hooks/' in file_path or any(f['name'].startswith('use') for f in functions): 749 | hook_names = [f['name'] for f in functions if f['name'].startswith('use')] 750 | if hook_names: 751 | purpose.append(f"This custom hook module implements {', '.join(hook_names)}") 752 | 753 | elif '/types/' in file_path or file_path.endswith('.d.ts'): 754 | purpose.append("This type definition file declares interfaces and types") 755 | if exports: 756 | features.append(f"defines {len(exports)} types/interfaces") 757 | 758 | elif '/tests/' in file_path or 'test' in name.lower() or 'spec' in name.lower(): 759 | purpose.append("This test file verifies functionality") 760 | if functions: 761 | features.append(f"contains {len(functions)} test cases") 762 | 763 | elif file_path.endswith(('.css', '.scss', '.less')): 764 | purpose.append("This style file defines visual appearance and layout") 765 | 766 | elif 'config' in name.lower() or file_path.endswith(('.json', '.env')): 767 | purpose.append("This configuration file manages project settings") 768 | 769 | else: 770 | # Default case - analyze based on content 771 | if functions: 772 | main_functions = [f['name'] for f in functions[:3]] 773 | purpose.append("This module implements application logic") 774 | features.append(f"key functions: {', '.join(main_functions)}") 775 | 776 | # Add integration details 777 | if imports: 778 | major_deps = [dep for dep in imports.keys() if not dep.startswith('.')][:2] 779 | if major_deps: 780 | integrations.append(f"integrates with {' and '.join(major_deps)}") 781 | 782 | # Combine all parts 783 | description = [] 784 | if purpose: 785 | description.extend(purpose) 786 | if features: 787 | description.append(f"It {', '.join(features)}") 788 | if integrations: 789 | description.extend(integrations) 790 | 791 | return '. '.join(description) + '.' 792 | 793 | def _extract_imports(self, content: str) -> Dict[str, set]: 794 | """Extract imports from file content""" 795 | imports = {} 796 | 797 | # Match ES6/TypeScript imports 798 | es6_pattern = r'import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+[\'"]([^\'"]+)[\'"]' 799 | for match in re.finditer(es6_pattern, content): 800 | module = match.group(1) 801 | imports[module] = set() 802 | 803 | # Match Python imports 804 | python_pattern = r'(?:from\s+([^\s]+)\s+import|import\s+([^\s]+))' 805 | for match in re.finditer(python_pattern, content): 806 | module = match.group(1) or match.group(2) 807 | imports[module] = set() 808 | 809 | return imports 810 | 811 | def _extract_exports(self, content: str) -> List[str]: 812 | """Extract exports from file content""" 813 | exports = [] 814 | 815 | # Match ES6/TypeScript exports 816 | es6_patterns = [ 817 | r'export\s+(?:default\s+)?(?:class|interface|type|const|let|var|function)\s+(\w+)', 818 | r'export\s+{\s*([^}]+)\s*}' 819 | ] 820 | 821 | for pattern in es6_patterns: 822 | for match in re.finditer(pattern, content): 823 | if ',' in match.group(1): 824 | exports.extend(name.strip() for name in match.group(1).split(',')) 825 | else: 826 | exports.append(match.group(1).strip()) 827 | 828 | # Match Python exports 829 | python_pattern = r'__all__\s*=\s*\[([^\]]+)\]' 830 | for match in re.finditer(python_pattern, content): 831 | exports.extend(name.strip().strip("'\"") for name in match.group(1).split(',')) 832 | 833 | return exports 834 | 835 | def _analyze_functionality(self, file_path: str, content: str, all_files_content: Dict[str, str]) -> List[Dict]: 836 | """Analyze functionality duplication and import/export mismatches""" 837 | alerts = [] 838 | 839 | # Extract imports and exports from current file 840 | current_imports = self._extract_imports(content) 841 | current_exports = self._extract_exports(content) 842 | 843 | # Look for functionality duplication 844 | for other_path, other_content in all_files_content.items(): 845 | if other_path == file_path: 846 | continue 847 | 848 | other_imports = self._extract_imports(other_content) 849 | other_exports = self._extract_exports(other_content) 850 | 851 | # Get relative paths for comparison 852 | rel_path = os.path.relpath(file_path) 853 | other_rel_path = os.path.relpath(other_path) 854 | 855 | # Extract and compare functions 856 | current_functions = self._extract_functions(content, os.path.splitext(file_path)[1][1:]) 857 | other_functions = self._extract_functions(other_content, os.path.splitext(other_path)[1][1:]) 858 | 859 | duplicates = self._find_duplicate_functions(current_functions, other_functions) 860 | if duplicates: 861 | details = [] 862 | for dup in duplicates: 863 | details.append(f"{dup['name']} ({int(dup['similarity']*100)}% similar - {dup['reason']})") 864 | 865 | alerts.append({ 866 | 'type': 'duplicate_functionality', 867 | 'file': other_path, 868 | 'details': f"These functions have similar implementations: {', '.join(details)}" 869 | }) 870 | 871 | # Check for circular dependencies 872 | if any(p in other_imports for p in [rel_path, os.path.splitext(rel_path)[0]]) and \ 873 | any(p in current_imports for p in [other_rel_path, os.path.splitext(other_rel_path)[0]]): 874 | alerts.append({ 875 | 'type': 'circular_dependency', 876 | 'file': other_path, 877 | 'details': "These files import each other, which could cause problems" 878 | }) 879 | 880 | # Check for mismatched imports 881 | for source, items in current_imports.items(): 882 | if source in [other_rel_path, os.path.splitext(other_rel_path)[0]]: 883 | missing_exports = items - other_exports - {'*'} 884 | if missing_exports: 885 | alerts.append({ 886 | 'type': 'import_mismatch', 887 | 'file': other_path, 888 | 'details': f"Trying to import {', '.join(sorted(missing_exports))} but they're not exported from the file" 889 | }) 890 | 891 | return alerts 892 | 893 | def generate_review(self, project_path: str) -> str: 894 | """Generate a file-by-file code review""" 895 | try: 896 | files = self.get_relevant_files(project_path) 897 | if not files: 898 | return "No relevant files found for analysis." 899 | 900 | # Filter out any CursorFocus files 901 | cursorfocus_path = os.path.join(project_path, 'CursorFocus') 902 | files = [f for f in files if not f.startswith(cursorfocus_path)] 903 | 904 | file_contents = {f: self.read_file_content(f) for f in files} 905 | file_analyses = {} 906 | for file_path, content in file_contents.items(): 907 | file_analyses[file_path] = self.analyze_file(file_path, content, file_contents) 908 | 909 | # Format the review 910 | formatted_review = "# Code Review Report\n\n" 911 | 912 | # Group files by directory 913 | by_directory = defaultdict(list) 914 | for file_path in files: 915 | dir_path = os.path.dirname(file_path) 916 | by_directory[dir_path].append(file_path) 917 | 918 | # Generate review for each directory 919 | for dir_path, dir_files in sorted(by_directory.items()): 920 | rel_dir = os.path.relpath(dir_path, project_path) 921 | formatted_review += f"\n## 📁 {rel_dir}\n" 922 | formatted_review += f"{self._get_directory_purpose(rel_dir)}\n\n" 923 | 924 | for file_path in sorted(dir_files): 925 | rel_path = os.path.relpath(file_path, project_path) 926 | analysis = file_analyses[file_path] 927 | content = file_contents[file_path] 928 | line_count = len(content.splitlines()) 929 | 930 | # Add empty line before each file entry 931 | formatted_review += "\n" 932 | 933 | # File header with line count 934 | formatted_review += f"`/{rel_path}` ({line_count} lines)\n" 935 | 936 | # Description 937 | formatted_review += "**What this file does:**\n" 938 | formatted_review += f"{analysis['explanation']}\n" 939 | 940 | # Alerts 941 | if analysis['functionality_alerts']: 942 | formatted_review += "**⚠️ Alerts:**\n" 943 | for alert in analysis['functionality_alerts']: 944 | if alert['type'] == 'length_warning': 945 | formatted_review += f"- 📏 {alert['details']} (Current: {alert['count']} lines)\n" 946 | elif alert['type'] == 'duplicate_functionality': 947 | locations = ', '.join(f'`{os.path.relpath(loc, project_path)}`' for loc in alert.get('locations', [])) 948 | formatted_review += f"- 🔄 {alert['details']} (Found in: {locations})\n" 949 | elif alert['type'] == 'similar_content': 950 | formatted_review += f"- 👯 {alert['details']}\n" 951 | 952 | # Key Functions 953 | functions = self._extract_functions(content, os.path.splitext(file_path)[1][1:]) 954 | if functions: 955 | formatted_review += "**Key Functions:**\n" 956 | for func in functions: 957 | description = self._generate_function_description(func['name'], '', os.path.splitext(file_path)[1][1:]) 958 | formatted_review += f"<{func['name']}>: {description}\n" 959 | 960 | # Related files 961 | if analysis['similar_files']: 962 | formatted_review += "**Related files:**\n" 963 | for similar in analysis['similar_files']: 964 | formatted_review += f"- Works with `{os.path.relpath(similar['file'], project_path)}`\n" 965 | 966 | formatted_review += "---\n\n\n" # Added an extra newline here 967 | 968 | formatted_review += f"\n## Review Date\n{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" 969 | return formatted_review 970 | 971 | except Exception as e: 972 | logging.error(f"Error in review generation: {str(e)}", exc_info=True) 973 | return f"Error generating review: {str(e)}" 974 | 975 | def _get_directory_purpose(self, dir_path: str) -> str: 976 | """Get a simple explanation of what a directory is for""" 977 | # Generic directory purposes 978 | common_dirs = { 979 | '.': "Project root directory", 980 | 'src': "Source code directory", 981 | 'components': "Reusable components", 982 | 'utils': "Utility functions", 983 | 'styles': "Style definitions", 984 | 'api': "API-related code", 985 | 'public': "Public assets", 986 | 'types': "Type definitions", 987 | 'tests': "Test files", 988 | 'docs': "Documentation", 989 | 'config': "Configuration files", 990 | 'lib': "Library code", 991 | 'assets': "Static assets", 992 | 'scripts': "Build and utility scripts" 993 | } 994 | 995 | # Handle nested paths 996 | parts = dir_path.split(os.sep) 997 | if len(parts) > 1: 998 | if parts[0] in common_dirs: 999 | base_purpose = common_dirs[parts[0]] 1000 | sub_path = '/'.join(parts[1:]) 1001 | return f"{base_purpose} - {sub_path}" 1002 | 1003 | return common_dirs.get(dir_path, f"Directory: {dir_path}") 1004 | 1005 | def is_binary_file(content: str) -> bool: 1006 | """Check if file content appears to be binary""" 1007 | try: 1008 | content.encode('utf-8') 1009 | return False 1010 | except UnicodeError: 1011 | return True 1012 | 1013 | def main(): 1014 | """Main function to generate code review.""" 1015 | logging.basicConfig(level=logging.INFO) 1016 | 1017 | # Load environment variables 1018 | from dotenv import load_dotenv 1019 | cursorfocus_env = os.path.join(os.path.dirname(__file__), '.env') 1020 | load_dotenv(cursorfocus_env) 1021 | 1022 | api_key = os.getenv('GEMINI_API_KEY') 1023 | if not api_key: 1024 | logging.error("GEMINI_API_KEY not found in environment variables") 1025 | return 1026 | 1027 | # Use parent directory as project path 1028 | project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 1029 | 1030 | # Generate review 1031 | reviewer = CodeReviewGenerator(api_key) 1032 | review_content = reviewer.generate_review(project_path) 1033 | 1034 | # Save review in project root 1035 | output_file = os.path.join(project_path, 'CodeReview.md') 1036 | try: 1037 | with open(output_file, 'w') as f: 1038 | f.write(review_content) 1039 | print(f"Code review saved to {output_file}") 1040 | except Exception as e: 1041 | logging.error(f"Error saving review: {str(e)}") 1042 | 1043 | if __name__ == '__main__': 1044 | main() -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "name": "Example Project", 5 | "project_path": "/path/to/your/project", 6 | "type": "node_js", 7 | "custom_rules": { 8 | "max_file_size": 500, 9 | "ignored_patterns": [ 10 | "*.test.ts", 11 | "*.spec.ts", 12 | "CursorFocus/*" 13 | ] 14 | }, 15 | "watch": true 16 | } 17 | ], 18 | "update_interval": 60, 19 | "max_depth": 3, 20 | "ignored_directories": [ 21 | "__pycache__", 22 | "node_modules", 23 | "venv", 24 | ".git", 25 | ".idea", 26 | ".vscode", 27 | "dist", 28 | "build", 29 | "CursorFocus" 30 | ], 31 | "ignored_files": [ 32 | ".DS_Store", 33 | "*.pyc", 34 | "*.pyo" 35 | ], 36 | "binary_extensions": [ 37 | ".png", 38 | ".jpg", 39 | ".jpeg", 40 | ".gif", 41 | ".ico", 42 | ".pdf", 43 | ".exe", 44 | ".bin" 45 | ], 46 | "file_length_standards": { 47 | ".js": 300, 48 | ".jsx": 250, 49 | ".ts": 300, 50 | ".tsx": 250, 51 | ".py": 400, 52 | ".css": 400, 53 | ".scss": 400, 54 | ".less": 400, 55 | ".sass": 400, 56 | ".html": 300, 57 | ".vue": 250, 58 | ".svelte": 250, 59 | ".json": 100, 60 | ".yaml": 100, 61 | ".yml": 100, 62 | ".toml": 100, 63 | ".md": 500, 64 | ".rst": 500, 65 | "default": 300 66 | }, 67 | "file_length_thresholds": { 68 | "warning": 1.0, 69 | "critical": 1.5, 70 | "severe": 2.0 71 | }, 72 | "project_types": { 73 | "chrome_extension": { 74 | "indicators": [ 75 | "manifest.json" 76 | ], 77 | "required_files": [], 78 | "description": "Chrome Extension" 79 | }, 80 | "node_js": { 81 | "indicators": [ 82 | "package.json" 83 | ], 84 | "required_files": [], 85 | "description": "Node.js Project" 86 | }, 87 | "python": { 88 | "indicators": [ 89 | "setup.py", 90 | "pyproject.toml" 91 | ], 92 | "required_files": [], 93 | "description": "Python Project" 94 | }, 95 | "react": { 96 | "indicators": [], 97 | "required_files": [ 98 | "src/App.js", 99 | "src/index.js" 100 | ], 101 | "description": "React Application" 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | def load_config(): 5 | """Load configuration from config.json.""" 6 | try: 7 | script_dir = os.path.dirname(os.path.abspath(__file__)) 8 | config_path = os.path.join(script_dir, 'config.json') 9 | 10 | if os.path.exists(config_path): 11 | with open(config_path, 'r') as f: 12 | return json.load(f) 13 | 14 | return get_default_config() 15 | except Exception as e: 16 | print(f"Error loading config: {e}") 17 | return None 18 | 19 | def get_default_config(): 20 | """Get default configuration settings.""" 21 | return { 22 | "project_path": "", 23 | "update_interval": 60, 24 | "max_depth": 3, 25 | "ignored_directories": [ 26 | "__pycache__", 27 | "node_modules", 28 | "venv", 29 | ".git", 30 | ".idea", 31 | ".vscode", 32 | "dist", 33 | "build", 34 | "coverage" 35 | ], 36 | "ignored_files": [ 37 | ".DS_Store", 38 | "Thumbs.db", 39 | "*.pyc", 40 | "*.pyo", 41 | "package-lock.json", 42 | "yarn.lock" 43 | ], 44 | "binary_extensions": [ 45 | ".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".exe", ".bin" 46 | ], 47 | "file_length_standards": { 48 | ".js": 300, 49 | ".jsx": 250, 50 | ".ts": 300, 51 | ".tsx": 250, 52 | ".py": 400, 53 | ".css": 400, 54 | ".scss": 400, 55 | ".less": 400, 56 | ".sass": 400, 57 | ".html": 300, 58 | ".vue": 250, 59 | ".svelte": 250, 60 | ".json": 100, 61 | ".yaml": 100, 62 | ".yml": 100, 63 | ".toml": 100, 64 | ".md": 500, 65 | ".rst": 500, 66 | "default": 300 67 | } 68 | } 69 | 70 | # Load configuration once at module level 71 | _config = load_config() 72 | 73 | # Binary file extensions that should be ignored 74 | BINARY_EXTENSIONS = set(_config.get('binary_extensions', [])) 75 | 76 | # Documentation and text files that shouldn't be analyzed for functions 77 | NON_CODE_EXTENSIONS = { 78 | '.md', '.txt', '.log', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', 79 | '.conf', '.config', '.markdown', '.rst', '.rdoc', '.csv', '.tsv' 80 | } 81 | 82 | # Extensions that should be analyzed for code 83 | CODE_EXTENSIONS = { 84 | '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.cpp', '.c', '.h', 85 | '.hpp', '.cs', '.go', '.rb', '.php' 86 | } 87 | 88 | # Regex patterns for function detection 89 | FUNCTION_PATTERNS = { 90 | 'standard': r'(?:^|\s+)(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?function)', 91 | 'arrow': r'(?:^|\s+)(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[^=])\s*=>', 92 | 'method': r'\b(\w+)\s*:\s*(?:async\s*)?function', 93 | 'class_method': r'(?:^|\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*{', 94 | 'object_property': r'(\w+)\s*:\s*(?:\([^)]*\)|[^=])\s*=>' 95 | } 96 | 97 | # Keywords that should not be treated as function names 98 | IGNORED_KEYWORDS = { 99 | 'if', 'switch', 'while', 'for', 'catch', 'finally', 'else', 'return', 100 | 'break', 'continue', 'case', 'default', 'to', 'from', 'import', 'as', 101 | 'try', 'except', 'raise', 'with', 'async', 'await', 'yield', 'assert', 102 | 'pass', 'del', 'print', 'in', 'is', 'not', 'and', 'or', 'lambda', 103 | 'global', 'nonlocal', 'class', 'def', 'n', 'lines', 'directly' 104 | } 105 | 106 | # Names of files and directories that should be ignored 107 | IGNORED_NAMES = set(_config.get('ignored_directories', [])) 108 | 109 | FILE_LENGTH_STANDARDS = _config.get('file_length_standards', {}) 110 | 111 | def get_file_length_limit(file_path): 112 | """Get the recommended line limit for a given file type.""" 113 | ext = os.path.splitext(file_path)[1].lower() 114 | return FILE_LENGTH_STANDARDS.get(ext, FILE_LENGTH_STANDARDS.get('default', 300)) -------------------------------------------------------------------------------- /content_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from analyzers import analyze_file_content, should_ignore_file, is_binary_file 4 | from project_detector import detect_project_type, get_project_description, get_file_type_info 5 | from config import get_file_length_limit, load_config 6 | 7 | class ProjectMetrics: 8 | def __init__(self): 9 | self.total_files = 0 10 | self.total_lines = 0 11 | self.files_by_type = {} 12 | self.lines_by_type = {} 13 | self.alerts = { 14 | 'warning': 0, 15 | 'critical': 0, 16 | 'severe': 0 17 | } 18 | self.duplicate_functions = 0 19 | 20 | def get_file_length_alert(line_count, limit, thresholds): 21 | """Get alert level based on file length and thresholds.""" 22 | ratio = line_count / limit 23 | if ratio >= thresholds.get('severe', 2.0): 24 | return 'severe', f"🚨 Critical-Length Alert: File is more than {int(thresholds['severe']*100)}% of recommended length" 25 | elif ratio >= thresholds.get('critical', 1.5): 26 | return 'critical', f"⚠️ High-Length Alert: File is more than {int(thresholds['critical']*100)}% of recommended length" 27 | elif ratio >= thresholds.get('warning', 1.0): 28 | return 'warning', f"📄 Length Alert: File exceeds recommended length" 29 | return None, None 30 | 31 | def generate_focus_content(project_path, config): 32 | """Generate the Focus file content.""" 33 | metrics = ProjectMetrics() 34 | thresholds = config.get('file_length_thresholds', { 35 | 'warning': 1.0, 36 | 'critical': 1.5, 37 | 'severe': 2.0 38 | }) 39 | 40 | project_type = detect_project_type(project_path) 41 | project_info = get_project_description(project_path) 42 | 43 | content = [ 44 | f"# Project Focus: {project_info['name']}", 45 | "", 46 | f"**Current Goal:** {project_info['description']}", 47 | "", 48 | "**Key Components:**" 49 | ] 50 | 51 | # Add directory structure 52 | structure = get_directory_structure(project_path, config['max_depth']) 53 | content.extend(structure_to_tree(structure)) 54 | 55 | content.extend([ 56 | "", 57 | "**Project Context:**", 58 | f"Type: {project_info['key_features'][1].replace('Type: ', '')}", 59 | f"Target Users: Users of {project_info['name']}", 60 | f"Main Functionality: {project_info['description']}", 61 | "Key Requirements:", 62 | *[f"- {feature}" for feature in project_info['key_features']], 63 | "", 64 | "**Development Guidelines:**", 65 | "- Keep code modular and reusable", 66 | "- Follow best practices for the project type", 67 | "- Maintain clean separation of concerns", 68 | "", 69 | "# File Analysis" 70 | ]) 71 | 72 | # Analyze each file 73 | first_file = True 74 | for root, _, files in os.walk(project_path): 75 | if any(ignored in root.split(os.path.sep) for ignored in config['ignored_directories']): 76 | continue 77 | 78 | for file in files: 79 | if any(file.endswith(ignored.replace('*', '')) for ignored in config['ignored_files']): 80 | continue 81 | 82 | file_path = os.path.join(root, file) 83 | rel_path = os.path.relpath(file_path, project_path) 84 | 85 | if is_binary_file(file_path): 86 | continue 87 | 88 | metrics.total_files += 1 89 | functions, line_count = analyze_file_content(file_path) 90 | 91 | if functions or line_count > 0: 92 | if not first_file: 93 | content.append("") 94 | else: 95 | first_file = False 96 | 97 | file_type, file_desc = get_file_type_info(file) 98 | content.append(f"`{rel_path}` ({line_count} lines)") 99 | content.append(f"**Main Responsibilities:** {file_desc}") 100 | 101 | # Update metrics 102 | ext = os.path.splitext(file)[1].lower() 103 | metrics.files_by_type[ext] = metrics.files_by_type.get(ext, 0) + 1 104 | metrics.lines_by_type[ext] = metrics.lines_by_type.get(ext, 0) + line_count 105 | metrics.total_lines += line_count 106 | 107 | if functions: 108 | content.append("**Key Functions:**") 109 | for func_name, description in functions: 110 | content.append(f"<{func_name}>: {description}") 111 | if "Duplicate Alert" in description: 112 | metrics.duplicate_functions += 1 113 | 114 | # Get file-specific length limit and check thresholds 115 | length_limit = get_file_length_limit(file_path) 116 | alert_level, alert_message = get_file_length_alert(line_count, length_limit, thresholds) 117 | if alert_level: 118 | metrics.alerts[alert_level] += 1 119 | content.append(f"**{alert_message} ({line_count} lines vs. recommended {length_limit})**") 120 | 121 | # Add metrics summary 122 | content.extend([ 123 | "", 124 | "# Project Metrics Summary", 125 | f"Total Files: {metrics.total_files}", 126 | f"Total Lines: {metrics.total_lines:,}", 127 | "", 128 | "**Files by Type:**", 129 | *[f"- {ext}: {count} files ({metrics.lines_by_type[ext]:,} lines)" 130 | for ext, count in sorted(metrics.files_by_type.items())], 131 | "", 132 | "**Code Quality Alerts:**", 133 | f"- 🚨 Severe Length Issues: {metrics.alerts['severe']} files", 134 | f"- ⚠️ Critical Length Issues: {metrics.alerts['critical']} files", 135 | f"- 📄 Length Warnings: {metrics.alerts['warning']} files", 136 | f"- 🔄 Duplicate Functions: {metrics.duplicate_functions}", 137 | "", 138 | f"Last updated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}" 139 | ]) 140 | 141 | return '\n'.join(content) 142 | 143 | def get_directory_structure(project_path, max_depth=3, current_depth=0): 144 | """Get the directory structure.""" 145 | if current_depth > max_depth: 146 | return {} 147 | 148 | structure = {} 149 | try: 150 | for item in os.listdir(project_path): 151 | if should_ignore_file(item): 152 | continue 153 | 154 | item_path = os.path.join(project_path, item) 155 | 156 | if os.path.isdir(item_path): 157 | substructure = get_directory_structure(item_path, max_depth, current_depth + 1) 158 | if substructure: 159 | structure[item] = substructure 160 | else: 161 | structure[item] = None 162 | except Exception as e: 163 | print(f"Error scanning directory {project_path}: {e}") 164 | 165 | return structure 166 | 167 | def structure_to_tree(structure, prefix=''): 168 | """Convert directory structure to tree format.""" 169 | lines = [] 170 | items = sorted(list(structure.items()), key=lambda x: (x[1] is not None, x[0])) 171 | 172 | for i, (name, substructure) in enumerate(items): 173 | is_last = i == len(items) - 1 174 | connector = '└─ ' if is_last else '├─ ' 175 | 176 | if substructure is None: 177 | icon = '📄 ' 178 | lines.append(f"{prefix}{connector}{icon}{name}") 179 | else: 180 | icon = '📁 ' 181 | lines.append(f"{prefix}{connector}{icon}{name}") 182 | extension = ' ' if is_last else '│ ' 183 | lines.extend(structure_to_tree(substructure, prefix + extension)) 184 | 185 | return lines -------------------------------------------------------------------------------- /focus.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime 4 | from .config import load_config, get_default_config 5 | from .content_generator import generate_focus_content 6 | from .rules_analyzer import RulesAnalyzer 7 | from .rules_generator import RulesGenerator 8 | 9 | def get_default_config(): 10 | """Get default configuration with parent directory as project path.""" 11 | return { 12 | 'project_path': os.path.abspath(os.path.join(os.path.dirname(__file__), '..')), 13 | 'update_interval': 60, 14 | 'max_depth': 3, 15 | 'ignored_directories': [ 16 | '__pycache__', 17 | 'node_modules', 18 | 'venv', 19 | '.git', 20 | '.idea', 21 | '.vscode', 22 | 'dist', 23 | 'build', 24 | 'CursorFocus' 25 | ], 26 | 'ignored_files': [ 27 | '.DS_Store', 28 | '*.pyc', 29 | '*.pyo' 30 | ], 31 | 'binary_extensions': [ 32 | '.png', 33 | '.jpg', 34 | '.jpeg', 35 | '.gif', 36 | '.ico', 37 | '.pdf', 38 | '.exe', 39 | '.bin' 40 | ], 41 | 'file_length_standards': { 42 | '.js': 300, 43 | '.jsx': 250, 44 | '.ts': 300, 45 | '.tsx': 250, 46 | '.py': 400, 47 | '.css': 400, 48 | '.scss': 400, 49 | '.less': 400, 50 | '.sass': 400, 51 | '.html': 300, 52 | '.vue': 250, 53 | '.svelte': 250, 54 | '.json': 100, 55 | '.yaml': 100, 56 | '.yml': 100, 57 | '.toml': 100, 58 | '.md': 500, 59 | '.rst': 500, 60 | 'default': 300 61 | }, 62 | 'file_length_thresholds': { 63 | 'warning': 1.0, 64 | 'critical': 1.5, 65 | 'severe': 2.0 66 | }, 67 | 'project_types': { 68 | 'chrome_extension': { 69 | 'indicators': ['manifest.json'], 70 | 'required_files': [], 71 | 'description': 'Chrome Extension' 72 | }, 73 | 'node_js': { 74 | 'indicators': ['package.json'], 75 | 'required_files': [], 76 | 'description': 'Node.js Project' 77 | }, 78 | 'python': { 79 | 'indicators': ['setup.py', 'pyproject.toml'], 80 | 'required_files': [], 81 | 'description': 'Python Project' 82 | }, 83 | 'react': { 84 | 'indicators': [], 85 | 'required_files': ['src/App.js', 'src/index.js'], 86 | 'description': 'React Application' 87 | } 88 | } 89 | } 90 | 91 | def setup_cursor_focus(project_path): 92 | """Set up CursorFocus for a project by generating necessary files.""" 93 | try: 94 | # Generate .cursorrules file 95 | print(f"Analyzing project: {project_path}") 96 | analyzer = RulesAnalyzer(project_path) 97 | project_info = analyzer.analyze_project_for_rules() 98 | 99 | rules_generator = RulesGenerator(project_path) 100 | rules_file = rules_generator.generate_rules_file(project_info) 101 | print(f"✅ Generated {rules_file}") 102 | 103 | # Generate initial Focus.md with default config 104 | focus_file = os.path.join(project_path, 'Focus.md') 105 | default_config = get_default_config() 106 | content = generate_focus_content(project_path, default_config) 107 | with open(focus_file, 'w', encoding='utf-8') as f: 108 | f.write(content) 109 | print(f"✅ Generated {focus_file}") 110 | 111 | print("\n🎉 CursorFocus setup complete!") 112 | print("Generated files:") 113 | print(f"- {rules_file}") 114 | print(f"- {focus_file}") 115 | except Exception as e: 116 | print(f"❌ Error during setup: {e}") 117 | raise 118 | 119 | def monitor_project(project_config, global_config): 120 | """Monitor a single project.""" 121 | project_path = project_config['project_path'] 122 | print(f"\n🔍 Monitoring project: {project_config['name']} at {project_path}") 123 | 124 | # Merge project config with global config 125 | config = {**global_config, **project_config} 126 | 127 | focus_file = os.path.join(project_path, 'Focus.md') 128 | last_content = None 129 | last_update = 0 130 | 131 | while True: 132 | current_time = time.time() 133 | 134 | if current_time - last_update < config.get('update_interval', 60): 135 | time.sleep(1) 136 | continue 137 | 138 | content = generate_focus_content(project_path, config) 139 | 140 | if content != last_content: 141 | try: 142 | with open(focus_file, 'w', encoding='utf-8') as f: 143 | f.write(content) 144 | last_content = content 145 | print(f"✅ {project_config['name']} Focus.md updated at {datetime.now().strftime('%I:%M:%S %p')}") 146 | except Exception as e: 147 | print(f"❌ Error writing Focus.md for {project_config['name']}: {e}") 148 | 149 | last_update = current_time 150 | 151 | def main(): 152 | """Main function to monitor multiple projects.""" 153 | config = load_config() 154 | if not config: 155 | print("No config.json found, using default configuration") 156 | config = get_default_config() 157 | 158 | if 'projects' not in config: 159 | # Handle single project config for backward compatibility 160 | config['projects'] = [{ 161 | 'name': 'Default Project', 162 | 'project_path': config['project_path'], 163 | 'update_interval': config.get('update_interval', 60), 164 | 'max_depth': config.get('max_depth', 3) 165 | }] 166 | 167 | # Create threads for each project 168 | from threading import Thread 169 | threads = [] 170 | 171 | try: 172 | for project in config['projects']: 173 | # Setup project if needed 174 | rules_file = os.path.join(project['project_path'], '.cursorrules') 175 | if not os.path.exists(rules_file): 176 | setup_cursor_focus(project['project_path']) 177 | 178 | # Start monitoring thread 179 | thread = Thread( 180 | target=monitor_project, 181 | args=(project, config), 182 | daemon=True 183 | ) 184 | thread.start() 185 | threads.append(thread) 186 | 187 | print("\n📝 Press Ctrl+C to stop all monitors") 188 | 189 | # Keep main thread alive 190 | while True: 191 | time.sleep(1) 192 | 193 | except KeyboardInterrupt: 194 | print("\n👋 Stopping all CursorFocus monitors") 195 | except Exception as e: 196 | print(f"\n❌ Error: {e}") 197 | 198 | if __name__ == '__main__': 199 | main() -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | BLUE='\033[0;34m' 6 | RED='\033[0;31m' 7 | NC='\033[0m' # No Color 8 | 9 | echo -e "${BLUE}🚀 Installing CursorFocus...${NC}" 10 | 11 | # Check if Python 3 is installed 12 | if ! command -v python3 &> /dev/null; then 13 | echo -e "${RED}❌ Python 3 is not installed. Please install Python 3 and try again.${NC}" 14 | exit 1 15 | fi 16 | 17 | # Create virtual environment if it doesn't exist 18 | if [ ! -d "venv" ]; then 19 | echo -e "${BLUE}📦 Creating virtual environment...${NC}" 20 | python3 -m venv venv 21 | fi 22 | 23 | # Activate virtual environment 24 | echo -e "${BLUE}🔌 Activating virtual environment...${NC}" 25 | source venv/bin/activate 26 | 27 | # Install dependencies 28 | echo -e "${BLUE}📚 Installing dependencies...${NC}" 29 | pip install -e . 30 | 31 | # Create .env file if it doesn't exist 32 | if [ ! -f ".env" ]; then 33 | echo -e "${BLUE}📝 Creating .env file...${NC}" 34 | cp .env.example .env 35 | echo -e "${GREEN}✅ Created .env file. Please edit it to add your Gemini API key.${NC}" 36 | echo -e " Get your API key from: ${BLUE}https://makersuite.google.com/app/apikey${NC}" 37 | fi 38 | 39 | # Create config.json if it doesn't exist 40 | if [ ! -f "config.json" ]; then 41 | echo -e "${BLUE}⚙️ Creating config.json...${NC}" 42 | cp config.example.json config.json 43 | echo -e "${GREEN}✅ Created config.json. Please edit it to configure your project settings.${NC}" 44 | fi 45 | 46 | echo -e "${GREEN}✅ Installation complete!${NC}" 47 | echo -e "\nNext steps:" 48 | echo -e "1. Edit ${BLUE}.env${NC} to add your Gemini API key" 49 | echo -e "2. Edit ${BLUE}config.json${NC} to configure your project settings" 50 | echo -e "3. Run ${BLUE}cursorfocus${NC} for continuous monitoring" 51 | echo -e " or ${BLUE}cursorfocus-review${NC} for a one-time code review" -------------------------------------------------------------------------------- /project_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from config import load_config 4 | 5 | # Load project types from config at module level 6 | _config = load_config() 7 | PROJECT_TYPES = _config.get('project_types', {}) 8 | 9 | def detect_project_type(project_path): 10 | """Detect project type based on file presence using configurable rules.""" 11 | for project_type, rules in PROJECT_TYPES.items(): 12 | # Check for indicator files 13 | if any(os.path.exists(os.path.join(project_path, f)) for f in rules.get('indicators', [])): 14 | return project_type 15 | 16 | # Check for required files 17 | if rules.get('required_files') and all(os.path.exists(os.path.join(project_path, f)) for f in rules['required_files']): 18 | return project_type 19 | 20 | return 'generic' 21 | 22 | def scan_for_projects(root_path, max_depth=3, ignored_dirs=None): 23 | """Scan directory recursively for projects.""" 24 | if ignored_dirs is None: 25 | ignored_dirs = _config.get('ignored_directories', []) 26 | 27 | projects = [] 28 | root_path = os.path.abspath(root_path or '.') 29 | 30 | # Kiểm tra thư mục gốc trước 31 | project_type = detect_project_type(root_path) 32 | if project_type != 'generic': 33 | projects.append({ 34 | 'path': root_path, 35 | 'type': project_type, 36 | 'name': os.path.basename(root_path) 37 | }) 38 | 39 | def _scan_directory(current_path, current_depth): 40 | if current_depth > max_depth: 41 | return 42 | 43 | try: 44 | # Skip ignored directories 45 | if any(ignored in current_path.split(os.path.sep) for ignored in ignored_dirs): 46 | return 47 | 48 | # Scan subdirectories 49 | for item in os.listdir(current_path): 50 | item_path = os.path.join(current_path, item) 51 | if os.path.isdir(item_path): 52 | # Kiểm tra từng thư mục con 53 | project_type = detect_project_type(item_path) 54 | if project_type != 'generic': 55 | projects.append({ 56 | 'path': item_path, 57 | 'type': project_type, 58 | 'name': item 59 | }) 60 | else: 61 | # Nếu không phải project thì quét tiếp 62 | _scan_directory(item_path, current_depth + 1) 63 | 64 | except (PermissionError, OSError): 65 | # Skip directories we can't access 66 | pass 67 | 68 | # Bắt đầu quét từ thư mục gốc 69 | _scan_directory(root_path, 0) 70 | return projects 71 | 72 | def get_project_description(project_path): 73 | """Get project description and key features using standardized approach.""" 74 | try: 75 | project_type = detect_project_type(project_path) 76 | project_info = { 77 | "name": os.path.basename(project_path), 78 | "description": "Project directory structure and information", 79 | "key_features": [ 80 | f"Type: {PROJECT_TYPES.get(project_type, {'description': 'Generic Project'})['description']}", 81 | "File and directory tracking", 82 | "Automatic updates" 83 | ] 84 | } 85 | 86 | if project_type == 'chrome_extension': 87 | manifest_path = os.path.join(project_path, 'manifest.json') 88 | if os.path.exists(manifest_path): 89 | with open(manifest_path, 'r') as f: 90 | manifest_data = json.load(f) 91 | project_info.update({ 92 | "name": manifest_data.get('name', 'Chrome Extension'), 93 | "description": manifest_data.get('description', 'No description available'), 94 | "key_features": [ 95 | f"Version: {manifest_data.get('version', 'unknown')}", 96 | f"Type: {PROJECT_TYPES[project_type]['description']}", 97 | *[f"Permission: {perm}" for perm in manifest_data.get('permissions', [])[:3]] 98 | ] 99 | }) 100 | 101 | elif project_type == 'node_js': 102 | package_path = os.path.join(project_path, 'package.json') 103 | if os.path.exists(package_path): 104 | with open(package_path, 'r') as f: 105 | package_data = json.load(f) 106 | project_info.update({ 107 | "name": package_data.get('name', 'Node.js Project'), 108 | "description": package_data.get('description', 'No description available'), 109 | "key_features": [ 110 | f"Version: {package_data.get('version', 'unknown')}", 111 | f"Type: {PROJECT_TYPES[project_type]['description']}", 112 | *[f"Dependency: {dep}" for dep in list(package_data.get('dependencies', {}).keys())[:3]] 113 | ] 114 | }) 115 | 116 | return project_info 117 | 118 | except Exception as e: 119 | print(f"Error getting project description: {e}") 120 | return { 121 | "name": os.path.basename(project_path), 122 | "description": "Error reading project information", 123 | "key_features": ["File and directory tracking"] 124 | } 125 | 126 | def get_file_type_info(filename): 127 | """Get file type information.""" 128 | ext = os.path.splitext(filename)[1].lower() 129 | 130 | type_map = { 131 | '.py': ('Python Source', 'Python script containing project logic'), 132 | '.js': ('JavaScript', 'JavaScript file for client-side functionality'), 133 | '.jsx': ('React Component', 'React component file'), 134 | '.ts': ('TypeScript', 'TypeScript source file'), 135 | '.tsx': ('React TypeScript', 'React component with TypeScript'), 136 | '.html': ('HTML', 'Web page template'), 137 | '.css': ('CSS', 'Stylesheet for visual styling'), 138 | '.md': ('Markdown', 'Documentation file'), 139 | '.json': ('JSON', 'Configuration or data file') 140 | } 141 | 142 | return type_map.get(ext, ('Generic', 'Project file')) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=6.0.1 2 | watchdog>=3.0.0 3 | python-dotenv>=1.0.0 4 | colorama>=0.4.6 5 | rich>=13.7.0 6 | google-generativeai>=0.3.0 -------------------------------------------------------------------------------- /rules_analyzer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from typing import Dict, Any 4 | 5 | class RulesAnalyzer: 6 | def __init__(self, project_path: str): 7 | self.project_path = project_path 8 | 9 | def analyze_project_for_rules(self) -> Dict[str, Any]: 10 | """Analyze the project and return project information for rules generation.""" 11 | project_info = { 12 | 'name': self._detect_project_name(), 13 | 'version': '1.0.0', 14 | 'language': self._detect_main_language(), 15 | 'framework': self._detect_framework(), 16 | 'type': self._detect_project_type() 17 | } 18 | return project_info 19 | 20 | def _detect_project_name(self) -> str: 21 | """Detect the project name from package files or directory name.""" 22 | # Try package.json 23 | package_json_path = os.path.join(self.project_path, 'package.json') 24 | if os.path.exists(package_json_path): 25 | try: 26 | with open(package_json_path, 'r') as f: 27 | data = json.load(f) 28 | if data.get('name'): 29 | return data['name'] 30 | except: 31 | pass 32 | 33 | # Try setup.py 34 | setup_py_path = os.path.join(self.project_path, 'setup.py') 35 | if os.path.exists(setup_py_path): 36 | try: 37 | with open(setup_py_path, 'r') as f: 38 | content = f.read() 39 | if 'name=' in content: 40 | # Simple extraction, could be improved 41 | name = content.split('name=')[1].split(',')[0].strip("'\"") 42 | if name: 43 | return name 44 | except: 45 | pass 46 | 47 | # Default to directory name 48 | return os.path.basename(os.path.abspath(self.project_path)) 49 | 50 | def _detect_main_language(self) -> str: 51 | """Detect the main programming language used in the project.""" 52 | extensions = {} 53 | 54 | for root, _, files in os.walk(self.project_path): 55 | if 'node_modules' in root or 'venv' in root or '.git' in root: 56 | continue 57 | 58 | for file in files: 59 | ext = os.path.splitext(file)[1].lower() 60 | if ext: 61 | extensions[ext] = extensions.get(ext, 0) + 1 62 | 63 | # Map extensions to languages 64 | language_map = { 65 | '.py': 'python', 66 | '.js': 'javascript', 67 | '.ts': 'typescript', 68 | '.jsx': 'javascript', 69 | '.tsx': 'typescript', 70 | '.java': 'java', 71 | '.rb': 'ruby', 72 | '.php': 'php', 73 | '.go': 'go' 74 | } 75 | 76 | # Find the most common language 77 | max_count = 0 78 | main_language = 'javascript' # default 79 | 80 | for ext, count in extensions.items(): 81 | if ext in language_map and count > max_count: 82 | max_count = count 83 | main_language = language_map[ext] 84 | 85 | return main_language 86 | 87 | def _detect_framework(self) -> str: 88 | """Detect the framework used in the project.""" 89 | # Check package.json for JS/TS frameworks 90 | package_json_path = os.path.join(self.project_path, 'package.json') 91 | if os.path.exists(package_json_path): 92 | try: 93 | with open(package_json_path, 'r') as f: 94 | data = json.load(f) 95 | deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})} 96 | 97 | if 'react' in deps: 98 | return 'react' 99 | if 'vue' in deps: 100 | return 'vue' 101 | if '@angular/core' in deps: 102 | return 'angular' 103 | if 'next' in deps: 104 | return 'next.js' 105 | if 'express' in deps: 106 | return 'express' 107 | except: 108 | pass 109 | 110 | # Check requirements.txt for Python frameworks 111 | requirements_path = os.path.join(self.project_path, 'requirements.txt') 112 | if os.path.exists(requirements_path): 113 | try: 114 | with open(requirements_path, 'r') as f: 115 | content = f.read().lower() 116 | if 'django' in content: 117 | return 'django' 118 | if 'flask' in content: 119 | return 'flask' 120 | if 'fastapi' in content: 121 | return 'fastapi' 122 | except: 123 | pass 124 | 125 | return 'none' 126 | 127 | def _detect_project_type(self) -> str: 128 | """Detect the type of project (web, mobile, library, etc.).""" 129 | package_json_path = os.path.join(self.project_path, 'package.json') 130 | 131 | if os.path.exists(package_json_path): 132 | try: 133 | with open(package_json_path, 'r') as f: 134 | data = json.load(f) 135 | deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})} 136 | 137 | # Check for mobile frameworks 138 | if 'react-native' in deps or '@ionic/core' in deps: 139 | return 'mobile application' 140 | 141 | # Check for desktop frameworks 142 | if 'electron' in deps: 143 | return 'desktop application' 144 | 145 | # Check if it's a library 146 | if data.get('name', '').startswith('@') or '-lib' in data.get('name', ''): 147 | return 'library' 148 | except: 149 | pass 150 | 151 | # Look for common web project indicators 152 | web_indicators = ['index.html', 'public/index.html', 'src/index.html'] 153 | for indicator in web_indicators: 154 | if os.path.exists(os.path.join(self.project_path, indicator)): 155 | return 'web application' 156 | 157 | return 'application' -------------------------------------------------------------------------------- /rules_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from typing import Dict, Any, List 4 | from datetime import datetime 5 | 6 | class RulesGenerator: 7 | def __init__(self, project_path: str): 8 | self.project_path = project_path 9 | self.template_path = os.path.join(os.path.dirname(__file__), 'templates', 'default.cursorrules.json') 10 | self.focus_template_path = os.path.join(os.path.dirname(__file__), 'templates', 'Focus.md') 11 | 12 | def _get_timestamp(self) -> str: 13 | """Get current timestamp in standard format.""" 14 | return datetime.now().strftime('%B %d, %Y at %I:%M %p') 15 | 16 | def generate_rules_file(self, project_info: Dict[str, Any]) -> str: 17 | """Generate the .cursorrules file based on project analysis.""" 18 | # Load template 19 | template = self._load_template() 20 | 21 | # Customize template 22 | rules = self._customize_template(template, project_info) 23 | 24 | # Write to file 25 | rules_file = os.path.join(self.project_path, '.cursorrules') 26 | with open(rules_file, 'w', encoding='utf-8') as f: 27 | json.dump(rules, f, indent=2) 28 | 29 | return rules_file 30 | 31 | def _load_template(self) -> Dict[str, Any]: 32 | """Load the default template.""" 33 | try: 34 | with open(self.template_path, 'r', encoding='utf-8') as f: 35 | return json.load(f) 36 | except Exception as e: 37 | print(f"Error loading template: {e}") 38 | return self._get_default_template() 39 | 40 | def _customize_template(self, template: Dict[str, Any], project_info: Dict[str, Any]) -> Dict[str, Any]: 41 | """Customize the template based on project analysis.""" 42 | rules = template.copy() 43 | 44 | # Add timestamp first 45 | rules['last_updated'] = self._get_timestamp() 46 | 47 | # Update project info 48 | rules['project'].update(project_info) 49 | 50 | # Add framework-specific rules 51 | if project_info['framework'] != 'none': 52 | framework_rules = self._get_framework_rules(project_info['framework']) 53 | rules['ai_behavior']['code_generation']['style']['prefer'].extend(framework_rules) 54 | 55 | # Add language-specific rules 56 | language_rules = self._get_language_rules(project_info['language']) 57 | rules['ai_behavior']['code_generation']['style']['prefer'].extend(language_rules) 58 | 59 | # Add project-type specific rules 60 | self._add_project_type_rules(rules, project_info['type']) 61 | 62 | # Update testing frameworks 63 | rules['ai_behavior']['testing']['frameworks'] = self._detect_testing_frameworks() 64 | 65 | return rules 66 | 67 | def _get_framework_rules(self, framework: str) -> List[str]: 68 | """Get framework-specific coding rules.""" 69 | framework_rules = { 70 | 'react': [ 71 | 'use functional components over class components', 72 | 'prefer hooks for state management', 73 | 'use memo for performance optimization' 74 | ], 75 | 'vue': [ 76 | 'use composition API', 77 | 'prefer ref/reactive for state management', 78 | 'use script setup syntax' 79 | ], 80 | 'angular': [ 81 | 'follow angular style guide', 82 | 'use observables for async operations', 83 | 'implement lifecycle hooks properly' 84 | ], 85 | 'django': [ 86 | 'follow Django best practices', 87 | 'use class-based views when appropriate', 88 | 'implement proper model relationships' 89 | ], 90 | 'flask': [ 91 | 'use Flask blueprints for organization', 92 | 'implement proper error handling', 93 | 'use Flask-SQLAlchemy for database operations' 94 | ] 95 | } 96 | return framework_rules.get(framework.lower(), []) 97 | 98 | def _get_language_rules(self, language: str) -> List[str]: 99 | """Get language-specific coding rules.""" 100 | language_rules = { 101 | 'python': [ 102 | 'follow PEP 8 guidelines', 103 | 'use type hints', 104 | 'prefer list comprehension when appropriate' 105 | ], 106 | 'javascript': [ 107 | 'use modern ES features', 108 | 'prefer arrow functions', 109 | 'use optional chaining' 110 | ], 111 | 'typescript': [ 112 | 'use strict type checking', 113 | 'leverage type inference', 114 | 'use interface over type when possible' 115 | ] 116 | } 117 | return language_rules.get(language.lower(), []) 118 | 119 | def _detect_testing_frameworks(self) -> List[str]: 120 | """Detect testing frameworks used in the project.""" 121 | testing_frameworks = [] 122 | 123 | # Check package.json for JS/TS testing frameworks 124 | package_json_path = os.path.join(self.project_path, 'package.json') 125 | if os.path.exists(package_json_path): 126 | try: 127 | with open(package_json_path, 'r') as f: 128 | data = json.load(f) 129 | deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})} 130 | 131 | if 'jest' in deps: 132 | testing_frameworks.append('jest') 133 | if 'mocha' in deps: 134 | testing_frameworks.append('mocha') 135 | if '@testing-library/react' in deps: 136 | testing_frameworks.append('testing-library') 137 | except: 138 | pass 139 | 140 | # Check requirements.txt for Python testing frameworks 141 | requirements_path = os.path.join(self.project_path, 'requirements.txt') 142 | if os.path.exists(requirements_path): 143 | try: 144 | with open(requirements_path, 'r') as f: 145 | content = f.read().lower() 146 | if 'pytest' in content: 147 | testing_frameworks.append('pytest') 148 | if 'unittest' in content: 149 | testing_frameworks.append('unittest') 150 | except: 151 | pass 152 | 153 | return testing_frameworks if testing_frameworks else ['jest'] # Default to jest 154 | 155 | def _add_project_type_rules(self, rules: Dict[str, Any], project_type: str): 156 | """Add project-type specific rules.""" 157 | type_rules = { 158 | 'web application': { 159 | 'accessibility': {'required': True}, 160 | 'performance': { 161 | 'prefer': [ 162 | 'code splitting', 163 | 'lazy loading', 164 | 'performance monitoring' 165 | ] 166 | } 167 | }, 168 | 'mobile application': { 169 | 'performance': { 170 | 'prefer': [ 171 | 'offline first', 172 | 'battery optimization', 173 | 'responsive design' 174 | ] 175 | } 176 | }, 177 | 'library': { 178 | 'documentation': {'required': True}, 179 | 'testing': {'coverage_threshold': 90} 180 | } 181 | } 182 | 183 | specific_rules = type_rules.get(project_type.lower()) 184 | if specific_rules: 185 | rules['ai_behavior'].update(specific_rules) 186 | 187 | def _get_default_template(self) -> Dict[str, Any]: 188 | """Get a default template if the template file cannot be loaded.""" 189 | return { 190 | "version": "1.0", 191 | "last_updated": self._get_timestamp(), 192 | "project": { 193 | "name": "Unknown Project", 194 | "version": "1.0.0", 195 | "language": "javascript", 196 | "framework": "none", 197 | "type": "application" 198 | }, 199 | "ai_behavior": { 200 | "code_generation": { 201 | "style": { 202 | "prefer": [], 203 | "avoid": [ 204 | "magic numbers", 205 | "nested callbacks", 206 | "hard-coded values" 207 | ] 208 | } 209 | }, 210 | "testing": { 211 | "required": True, 212 | "frameworks": ["jest"], 213 | "coverage_threshold": 80 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | BLUE='\033[0;34m' 6 | RED='\033[0;31m' 7 | NC='\033[0m' # No Color 8 | 9 | # Get the directory where the script is located 10 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | 12 | # Help message 13 | show_help() { 14 | echo "Usage: ./run.sh [options]" 15 | echo "" 16 | echo "Options:" 17 | echo " --scan [path] Scan directory for projects (default: current directory)" 18 | echo " --help Show this help message" 19 | echo "" 20 | echo "Examples:" 21 | echo " ./run.sh # Run with default configuration" 22 | echo " ./run.sh --scan # Scan current directory for projects" 23 | echo " ./run.sh --scan ~/projects # Scan specific directory for projects" 24 | } 25 | 26 | # Parse command line arguments 27 | SCAN_MODE=false 28 | SCAN_PATH="." 29 | 30 | while [[ $# -gt 0 ]]; do 31 | case $1 in 32 | --scan) 33 | SCAN_MODE=true 34 | if [ ! -z "$2" ] && [ ${2:0:1} != "-" ]; then 35 | SCAN_PATH="$2" 36 | shift 37 | fi 38 | shift 39 | ;; 40 | --help) 41 | show_help 42 | exit 0 43 | ;; 44 | *) 45 | echo -e "${RED}Unknown option: $1${NC}" 46 | show_help 47 | exit 1 48 | ;; 49 | esac 50 | done 51 | 52 | echo -e "${BLUE}🚀 Starting CursorFocus...${NC}" 53 | 54 | # Check if Python 3 is installed 55 | if ! command -v python3 &> /dev/null; then 56 | echo -e "${RED}❌ Python 3 is not installed. Please install Python 3 and try again.${NC}" 57 | exit 1 58 | fi 59 | 60 | # Check if required Python packages are installed 61 | echo -e "${BLUE}📦 Checking dependencies...${NC}" 62 | pip3 install -r "$SCRIPT_DIR/requirements.txt" > /dev/null 2>&1 63 | 64 | if [ "$SCAN_MODE" = true ]; then 65 | echo -e "${BLUE}🔍 Scanning for projects in: $SCAN_PATH${NC}" 66 | python3 "$SCRIPT_DIR/setup.py" --scan "$SCAN_PATH" 67 | exit $? 68 | fi 69 | 70 | # Get the parent directory (project root) 71 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 72 | 73 | # Check if config.json exists, if not create it from example 74 | if [ ! -f "$SCRIPT_DIR/config.json" ]; then 75 | echo -e "${BLUE}📝 Creating configuration from template...${NC}" 76 | if [ -f "$SCRIPT_DIR/config.example.json" ]; then 77 | # Create config.json from example and replace placeholder path 78 | sed "s|/path/to/your/project|$PROJECT_ROOT|g" "$SCRIPT_DIR/config.example.json" > "$SCRIPT_DIR/config.json" 79 | echo -e "${GREEN}✅ Configuration created from template${NC}" 80 | else 81 | echo -e "${RED}❌ config.example.json not found. Please check the installation.${NC}" 82 | exit 1 83 | fi 84 | fi 85 | 86 | # Run CursorFocus 87 | echo -e "${BLUE}🔍 Starting CursorFocus monitor...${NC}" 88 | cd "$PROJECT_ROOT" 89 | python3 "$SCRIPT_DIR/focus.py" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="cursorfocus", 5 | version="1.0.0", 6 | packages=find_packages(), 7 | install_requires=[ 8 | "PyYAML>=6.0.1", 9 | "watchdog>=3.0.0", 10 | "python-dotenv>=1.0.0", 11 | "colorama>=0.4.6", 12 | "rich>=13.7.0", 13 | "google-generativeai>=0.3.0" 14 | ], 15 | entry_points={ 16 | "console_scripts": [ 17 | "cursorfocus=cursorfocus.focus:main", 18 | "cursorfocus-review=cursorfocus.code_review:main" 19 | ] 20 | }, 21 | author="Dror Bengal", 22 | author_email="your.email@example.com", 23 | description="AI-powered code review and project analysis tool", 24 | long_description=open("README.md").read(), 25 | long_description_content_type="text/markdown", 26 | url="https://github.com/Dror-Bengal/CursorFocus", 27 | classifiers=[ 28 | "Development Status :: 5 - Production/Stable", 29 | "Intended Audience :: Developers", 30 | "Topic :: Software Development :: Documentation", 31 | "Topic :: Software Development :: Quality Assurance", 32 | "License :: OSI Approved :: MIT License", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | ], 39 | python_requires=">=3.8", 40 | include_package_data=True, 41 | package_data={ 42 | "cursorfocus": ["templates/*"], 43 | } 44 | ) -------------------------------------------------------------------------------- /templates/default.cursorrules.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "last_updated": "", 4 | "project": { 5 | "name": "Your Project", 6 | "version": "1.0.0", 7 | "language": "javascript", 8 | "framework": "none", 9 | "type": "application" 10 | }, 11 | "ai_behavior": { 12 | "code_generation": { 13 | "style": { 14 | "prefer": [ 15 | "async/await over callbacks", 16 | "const over let", 17 | "descriptive variable names", 18 | "single responsibility functions" 19 | ], 20 | "avoid": [ 21 | "magic numbers", 22 | "nested callbacks", 23 | "hard-coded values", 24 | "complex conditionals" 25 | ] 26 | }, 27 | "error_handling": { 28 | "prefer": [ 29 | "try/catch for async operations", 30 | "custom error messages", 31 | "meaningful error states" 32 | ], 33 | "avoid": [ 34 | "silent errors", 35 | "empty catch blocks", 36 | "generic error messages" 37 | ] 38 | }, 39 | "performance": { 40 | "prefer": [ 41 | "lazy loading", 42 | "debouncing and throttling for events", 43 | "memoization for expensive calculations" 44 | ], 45 | "avoid": [ 46 | "blocking synchronous code", 47 | "large inline scripts", 48 | "unnecessary re-renders" 49 | ] 50 | } 51 | }, 52 | "testing": { 53 | "required": true, 54 | "frameworks": ["jest"], 55 | "coverage_threshold": 80, 56 | "include": [ 57 | "unit tests for new functions", 58 | "integration tests for critical workflows", 59 | "edge case scenarios" 60 | ] 61 | }, 62 | "security": { 63 | "sensitive_patterns": [ 64 | "API_KEY", 65 | "SECRET", 66 | "PASSWORD", 67 | "CREDENTIAL" 68 | ], 69 | "protected_files": [ 70 | "config/*.json", 71 | ".env*" 72 | ], 73 | "sanitize_input": true, 74 | "validate_user_data": true, 75 | "avoid_eval": true 76 | }, 77 | "accessibility": { 78 | "standards": ["WCAG 2.1"], 79 | "require_alt_text": true, 80 | "focus_indicators": true, 81 | "aria_labels": true 82 | } 83 | }, 84 | "communication": { 85 | "style": "step-by-step", 86 | "level": "beginner-friendly", 87 | "on_error": [ 88 | "log error details", 89 | "suggest alternative solutions", 90 | "ask for clarification if unsure" 91 | ], 92 | "on_success": [ 93 | "summarize changes", 94 | "provide context for future improvements", 95 | "highlight any potential optimizations" 96 | ], 97 | "confirmations": { 98 | "required_for": [ 99 | "major changes", 100 | "file deletions", 101 | "dependency updates", 102 | "structural changes" 103 | ] 104 | } 105 | }, 106 | "response_format": { 107 | "always": [ 108 | "show file paths", 109 | "explain changes simply", 110 | "highlight modified sections only", 111 | "provide next steps" 112 | ], 113 | "never": [ 114 | "create new files without permission", 115 | "remove existing code without confirmation", 116 | "use technical jargon without explanation", 117 | "show entire files unless requested" 118 | ] 119 | } 120 | } -------------------------------------------------------------------------------- /~/Library/LaunchAgents/com.cursorfocus.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.cursorfocus 7 | ProgramArguments 8 | 9 | /usr/local/bin/python3 10 | /Users/drorbengal/test1212/CursorFocus/focus.py 11 | 12 | WorkingDirectory 13 | /Users/drorbengal/test1212/CursorFocus 14 | EnvironmentVariables 15 | 16 | PATH 17 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 18 | 19 | RunAtLoad 20 | 21 | KeepAlive 22 | 23 | StandardOutPath 24 | /Users/drorbengal/test1212/CursorFocus/cursorfocus.log 25 | StandardErrorPath 26 | /Users/drorbengal/test1212/CursorFocus/cursorfocus.error.log 27 | 28 | --------------------------------------------------------------------------------