Skip to content

Instantly share code, notes, and snippets.

@floahs-ark
Created January 22, 2025 14:22
Show Gist options
  • Save floahs-ark/ca685df2da5bc6a60d0a3b2fa3fad55a to your computer and use it in GitHub Desktop.
Save floahs-ark/ca685df2da5bc6a60d0a3b2fa3fad55a to your computer and use it in GitHub Desktop.
Repository: dror-bengal/cursorfocus Files analyzed: 22 Estimated tokens: 26.6k
================================================
File: README.md
================================================
# CursorFocus
An AI-powered code review and project analysis tool that provides intelligent, contextual descriptions of your codebase.
## Features
- 🔄 Automated Code Reviews with AI-powered insights
- 📝 Intelligent file and function documentation
- 🌳 Project structure analysis and visualization
- 📏 Code quality metrics and alerts
- 🎯 Smart project type detection
- 🔍 Duplicate code detection
- 🧩 Modular and extensible design
- 🎛️ Customizable rules and configurations
- 🔄 Real-time project monitoring
## Installation
1. Clone the repository:
```bash
git clone https://github.com/Dror-Bengal/CursorFocus.git
```
2. Create and activate a virtual environment (recommended):
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install the package in development mode:
```bash
cd CursorFocus
pip install -e .
```
4. Create a `.env` file from the template:
```bash
cp .env.example .env
```
5. Add your Gemini API key to the `.env` file:
```
GEMINI_API_KEY=your_api_key_here
```
## Usage
### Generate a Code Review
From your project directory:
```bash
cursorfocus-review
```
This will generate a `CodeReview.md` file in your project root with:
- Project structure analysis
- File-by-file review
- Function documentation
- Code duplication alerts
- Project metrics
### Monitor Project Changes
To start real-time project monitoring:
```bash
cursorfocus
```
This will create and update a `Focus.md` file in your project root with:
- Current project state
- Directory structure
- File analysis
- Development guidelines
## Configuration
You can customize CursorFocus by creating a `config.json` file in your project root:
```json
{
"ignored_directories": [
"node_modules",
"venv",
".git"
],
"ignored_files": [
"*.pyc",
".DS_Store"
],
"max_depth": 3,
"update_interval": 60
}
```
## Requirements
- Python 3.8 or higher
- Google Gemini API key
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history.
================================================
File: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
## [1.0.0] - 2024-01-19
### Added
- AI-powered code review functionality using Gemini API
- Intelligent file and function documentation
- Duplicate code detection
- Security analysis features
- Project structure analysis
- Automated .cursorrules generation
- Real-time project monitoring
### Changed
- Improved documentation with clear setup instructions
- Enhanced error handling and API usage
- Restructured project for better maintainability
### Fixed
- API key handling and security
- File path handling across different OS
- Generated file formatting
## [0.1.0] - 2024-01-18
### Added
- Initial release
- Basic project structure monitoring
- File change detection
- Simple documentation generation
================================================
File: CONTRIBUTING.md
================================================
# Contributing to CursorFocus
We love your input! We want to make contributing to CursorFocus as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
1. Fork the repo and create your branch from `main`
2. If you've added code that should be tested, add tests
3. If you've changed APIs, update the documentation
4. Ensure the test suite passes
5. Make sure your code lints
6. Issue that pull request!
## Any contributions you make will be under the MIT Software License
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.
## Report bugs using GitHub's [issue tracker]
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## Use a Consistent Coding Style
* 4 spaces for indentation rather than tabs
* You can try running `pylint` for style unification
* Keep functions focused and single-purpose
* Document complex logic
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
================================================
File: LICENSE
================================================
MIT License
Copyright (c) 2024 Dror Bengal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
File: SHOWCASE.md
================================================
# CursorFocus Showcase
This showcase demonstrates the key features and capabilities of CursorFocus using real examples.
## Example Output
Below is an example of a Focus.md file generated for a React project:
```markdown
# Project Focus: E-commerce Dashboard
**Current Goal:** Build a modern e-commerce dashboard with real-time analytics and inventory management.
**Key Components:**
📁 src/
├── components/
│ ├── Dashboard/
│ ├── Analytics/
│ └── Inventory/
├── services/
│ └── api/
└── utils/
**Project Context:**
Type: React Application
Target Users: E-commerce store managers and administrators
Main Functionality: Real-time sales tracking and inventory management
Key Requirements:
- Real-time sales dashboard
- Inventory management system
- Analytics visualization
- User authentication
**Development Guidelines:**
- Keep code modular and reusable
- Follow React best practices
- Maintain clean separation of concerns
# File Analysis
`src/components/Dashboard/SalesOverview.tsx` (280 lines)
**Main Responsibilities:** Main dashboard component displaying sales metrics and KPIs
**Key Functions:**
<useSalesData>: Custom hook for fetching and processing real-time sales data from the API
<SalesChart>: Renders an interactive chart showing daily/monthly sales trends with customizable date ranges
<KPIGrid>: Displays key performance indicators in a responsive grid layout with real-time updates
`src/services/api/salesApi.ts` (180 lines)
**Main Responsibilities:** API service for sales-related operations
**Key Functions:**
<fetchSalesData>: Retrieves sales data with support for filtering and date ranges
<processSalesMetrics>: Processes raw sales data to calculate various business metrics
<aggregateByPeriod>: Aggregates sales data by day, week, or month for trend analysis
`src/components/Inventory/ProductList.tsx` (320 lines)
**Main Responsibilities:** Product inventory management interface
**Key Functions:**
<useInventoryData>: Manages inventory data state and operations
<ProductTable>: Renders a sortable and filterable table of products
<StockAlerts>: Displays alerts for low stock items and inventory issues
**📄 Long-file Alert: File exceeds the recommended 250 lines for .tsx files (320 lines)**
Last updated: December 28, 2023 at 11:45 PM
```
## Feature Highlights
### 1. Smart Project Type Detection
CursorFocus automatically detects your project type and provides relevant information:
- React/Node.js projects: Component structure, hooks, and API endpoints
- Python projects: Classes, functions, and module relationships
- Chrome Extensions: Manifest details and extension components
### 2. File Length Standards
The tool provides customized alerts based on file type:
```markdown
**📄 Long-file Alert: File exceeds the recommended 250 lines for .tsx files (320 lines)**
```
### 3. Detailed Function Analysis
Functions are analyzed with context-aware descriptions:
```markdown
<useSalesData>: Custom hook for fetching and processing real-time sales data from the API
<ProductTable>: Renders a sortable and filterable table of products with pagination
```
### 4. Directory Visualization
Clear, hierarchical representation of your project structure:
```markdown
📁 src/
├── components/
│ ├── Dashboard/
│ ├── Analytics/
│ └── Inventory/
├── services/
│ └── api/
└── utils/
```
### 5. Project Context
Comprehensive project overview with key information:
```markdown
Type: React Application
Target Users: E-commerce store managers
Main Functionality: Real-time sales tracking
```
## Real-World Use Cases
### 1. Onboarding New Developers
- Quick project overview and structure understanding
- Identification of key components and their responsibilities
- Clear view of coding standards and file organization
### 2. Code Review and Maintenance
- File length monitoring for maintainability
- Function documentation for better understanding
- Project structure visualization for navigation
### 3. Technical Documentation
- Automated documentation generation
- Real-time updates as code changes
- Consistent format across projects
### 4. Project Management
- Progress tracking through file and function analysis
- Code organization oversight
- Standards compliance monitoring
## Tips for Best Results
1. **File Organization:**
- Keep related files in appropriate directories
- Use meaningful file names
- Maintain a clean project structure
2. **Function Documentation:**
- Write clear function names
- Add descriptive comments
- Follow consistent documentation patterns
3. **Configuration:**
- Customize ignored directories for your needs
- Adjust file length standards if needed
- Set appropriate scan depth for your project
4. **Regular Updates:**
- Keep CursorFocus running for real-time updates
- Review the Focus.md file periodically
- Use alerts to maintain code quality
================================================
File: __init__.py
================================================
"""
CursorFocus - AI-powered code review and project analysis tool
"""
__version__ = "1.0.0"
__author__ = "Dror Bengal"
from .code_review import CodeReviewGenerator
from .focus import Focus
from .analyzers import CodeAnalyzer
from .project_detector import ProjectDetector
__all__ = [
'CodeReviewGenerator',
'Focus',
'CodeAnalyzer',
'ProjectDetector'
]
================================================
File: analyzers.py
================================================
import os
import re
from config import (
BINARY_EXTENSIONS,
IGNORED_NAMES,
NON_CODE_EXTENSIONS,
CODE_EXTENSIONS,
FUNCTION_PATTERNS,
IGNORED_KEYWORDS
)
import logging
def get_combined_pattern():
"""Combine all function patterns into a single regex pattern."""
return '|'.join(f'(?:{pattern})' for pattern in FUNCTION_PATTERNS.values())
def is_binary_file(filename):
"""Check if a file is binary or non-code based on its extension."""
ext = os.path.splitext(filename)[1].lower()
# Binary extensions
if ext in BINARY_EXTENSIONS:
return True
# Documentation and text files that shouldn't be analyzed for functions
return ext in NON_CODE_EXTENSIONS
def should_ignore_file(name):
"""Check if a file or directory should be ignored."""
return name in IGNORED_NAMES or name.startswith('.')
def find_duplicate_functions(content, filename):
"""Find duplicate functions in a file and their line numbers."""
duplicates = {}
function_lines = {}
# Combined pattern for all function types
combined_pattern = get_combined_pattern()
# Find all function declarations
for i, line in enumerate(content.split('\n'), 1):
matches = re.finditer(combined_pattern, line)
for match in matches:
# Get the first non-None group (the function name)
func_name = next(filter(None, match.groups()), None)
if func_name and func_name.lower() not in IGNORED_KEYWORDS:
if func_name not in function_lines:
function_lines[func_name] = []
function_lines[func_name].append(i)
# Identify duplicates with simplified line reporting
for func_name, lines in function_lines.items():
if len(lines) > 1:
# Only store first occurrence and count
duplicates[func_name] = (lines[0], len(lines))
return duplicates
def parse_comments(content_lines, start_index=0):
"""Parse both multi-line and single-line comments from a list of content lines.
Args:
content_lines: List of content lines to parse
start_index: Starting index to parse from (default: 0)
Returns:
list: List of cleaned comment lines
"""
description = []
in_comment_block = False
for line in reversed(content_lines[max(0, start_index):]):
line = line.strip()
# Handle JSDoc style comments
if line.startswith('/**'):
in_comment_block = True
continue
elif line.startswith('*/'):
continue
elif in_comment_block and line.startswith('*'):
cleaned_line = line.lstrip('* ').strip()
if cleaned_line and not cleaned_line.startswith('@'):
description.insert(0, cleaned_line)
# Handle single line comments
elif line.startswith('//'):
cleaned_line = line.lstrip('/ ').strip()
if cleaned_line:
description.insert(0, cleaned_line)
# Stop if we hit code
elif line and not line.startswith('/*') and not in_comment_block:
break
return description
def extract_function_context(content, start_pos, end_pos=None):
"""Extract and analyze the function's content to generate a meaningful description.
Args:
content: Full file content
start_pos: Starting position of the function
end_pos: Optional ending position of the function
Returns:
str: A user-friendly description of the function
"""
# Get more context before and after the function
context_before = content[max(0, start_pos-1000):start_pos].strip()
# Get the next 1000 characters after function declaration to analyze
context_length = 1000 if end_pos is None else end_pos - start_pos
context = content[start_pos:start_pos + context_length]
# Try to find function body between first { and matching }
body_start = context.find('{')
if body_start != -1:
bracket_count = 1
body_end = body_start + 1
while bracket_count > 0 and body_end < len(context):
if context[body_end] == '{':
bracket_count += 1
elif context[body_end] == '}':
bracket_count -= 1
body_end += 1
function_body = context[body_start:body_end].strip('{}')
else:
# For arrow functions or other formats
function_body = context.split('\n')[0]
# Extract parameters with their types/descriptions
params_match = re.search(r'\((.*?)\)', context)
parameters = []
param_descriptions = {}
if params_match:
params = params_match.group(1).split(',')
for param in params:
param = param.strip()
if param:
# Look for JSDoc param descriptions in context before
param_name = param.split(':')[0].strip().split('=')[0].strip()
param_desc_match = re.search(rf'@param\s+{{\w+}}\s+{param_name}\s+-?\s*([^\n]+)', context_before)
if param_desc_match:
param_descriptions[param_name] = param_desc_match.group(1).strip()
# Make parameter names readable
readable_param = re.sub(r'([A-Z])', r' \1', param_name).lower()
readable_param = readable_param.replace('_', ' ')
parameters.append(readable_param)
# Look for return value and its description
return_matches = re.findall(r'return\s+([^;]+)', function_body)
return_info = []
return_desc_match = re.search(r'@returns?\s+{[^}]+}\s+([^\n]+)', context_before)
if return_desc_match:
return_info.append(return_desc_match.group(1).strip())
elif return_matches:
for ret in return_matches:
ret = ret.strip()
if ret and not ret.startswith('{') and len(ret) < 50:
return_info.append(ret)
# Look for constants or enums being used
const_matches = re.findall(r'(?:const|enum)\s+(\w+)\s*=\s*{([^}]+)}', context_before)
constants = {}
for const_name, const_values in const_matches:
values = re.findall(r'(\w+):\s*([^,]+)', const_values)
if values:
constants[const_name] = values
# Analyze the actual purpose of the function
purpose = []
# Check for validation logic
if re.search(r'(valid|invalid|check|verify|test)\w*', function_body, re.I):
conditions = []
# Look for specific conditions being checked
condition_matches = re.findall(r'if\s*\((.*?)\)', function_body)
for cond in condition_matches[:2]: # Get first two conditions
cond = cond.strip()
if len(cond) < 50 and '&&' not in cond and '||' not in cond:
conditions.append(cond.replace('!', 'not '))
if conditions:
purpose.append(f"validates {' and '.join(conditions)}")
else:
purpose.append("validates input")
# Check for scoring/calculation logic with tiers
if re.search(r'TIER_\d+|score|calculate|compute', function_body, re.I):
# Look for tier assignments
tier_matches = re.findall(r'return\s+(\w+)\.TIER_(\d+)', function_body)
if tier_matches:
tiers = [f"Tier {tier}" for _, tier in tier_matches]
if constants and 'TIER_SCORES' in constants:
tier_info = []
for tier_name, tier_score in constants['TIER_SCORES']:
if any(t in tier_name for t in tiers):
tier_info.append(f"{tier_name.lower()}: {tier_score}")
if tier_info:
purpose.append(f"assigns scores ({', '.join(tier_info)})")
else:
purpose.append(f"assigns {' or '.join(tiers)} scores")
else:
# Look for other score calculations
calc_matches = re.findall(r'(\w+(?:Score|Rating|Value))\s*[+\-*/]=\s*([^;]+)', function_body)
if calc_matches:
calc_vars = [match[0] for match in calc_matches if len(match[0]) < 30]
if calc_vars:
purpose.append(f"calculates {' and '.join(calc_vars)}")
# Check for store validation
if re.search(r'store|domain|source', function_body, re.I):
store_checks = []
# Look for store list checks
if 'STORE_CATEGORIES' in constants:
store_types = [store[0] for store in constants['STORE_CATEGORIES']]
if store_types:
store_checks.append(f"checks against {', '.join(store_types)}")
# Look for domain validation
domain_checks = re.findall(r'\.(includes|match(?:es)?)\(([^)]+)\)', function_body)
if domain_checks:
store_checks.append("validates domain format")
if store_checks:
purpose.append(" and ".join(store_checks))
# Check for data transformation
if re.search(r'(map|filter|reduce|transform|convert|parse|format|normalize)', function_body, re.I):
transform_matches = re.findall(r'(\w+)\s*\.\s*(map|filter|reduce)', function_body)
if transform_matches:
items = [match[0] for match in transform_matches if len(match[0]) < 20]
if items:
purpose.append(f"processes {' and '.join(items)}")
# Look for specific number ranges and their context
range_matches = re.findall(r'([<>]=?)\s*(\d+)', function_body)
ranges = []
for op, num in range_matches:
# Look for variable name or context before comparison
context_match = re.search(rf'\b(\w+)\s*{op}\s*{num}', function_body)
if context_match:
var_name = context_match.group(1)
var_name = re.sub(r'([A-Z])', r' \1', var_name).lower()
ranges.append(f"{var_name} {op} {num}")
# Generate a user-friendly description
description_parts = []
# Add main purpose if found
if purpose:
description_parts.append(f"This function {' and '.join(purpose)}")
# Add parameter descriptions if available
if param_descriptions:
desc = []
for param, description in param_descriptions.items():
if len(description) < 50: # Keep only concise descriptions
desc.append(f"{param}: {description}")
if desc:
description_parts.append(f"Takes {', '.join(desc)}")
elif parameters:
description_parts.append(f"Takes {' and '.join(parameters)}")
# Add range information if found
if ranges:
description_parts.append(f"Ensures {' and '.join(ranges)}")
# Add return description if available
if return_info:
description_parts.append(f"Returns {return_info[0]}")
# If we couldn't generate a good description, return a simple one
if not description_parts:
return "This function helps with the program's functionality"
return " | ".join(description_parts)
def analyze_file_content(file_path):
"""Analyze file content for functions and their descriptions."""
try:
# Skip binary and non-code files
if is_binary_file(file_path):
return [], 0
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Skip files that don't look like actual code files
ext = os.path.splitext(file_path)[1].lower()
if ext not in CODE_EXTENSIONS:
return [], 0
functions = []
duplicates = find_duplicate_functions(content, file_path)
# Use combined pattern for function detection
combined_pattern = get_combined_pattern()
matches = re.finditer(combined_pattern, content, re.MULTILINE | re.DOTALL)
for match in matches:
func_name = next(filter(None, match.groups()), None)
if not func_name or func_name.lower() in IGNORED_KEYWORDS:
continue
# Get comment block before function
start = match.start()
comment_block = content[:start].strip().split('\n')[-10:] # Get up to 10 lines before function
description = parse_comments(comment_block)
# If no comment found or comment is too generic, analyze function content
if not description or len(description[0].split()) < 5:
# Extract detailed context from function body
context_description = extract_function_context(content, start)
# Analyze function name parts for additional context
name_parts = re.findall('[A-Z][a-z]*|[a-z]+', func_name)
verb = name_parts[0].lower() if name_parts else ''
subject = ' '.join(name_parts[1:]).lower() if len(name_parts) > 1 else ''
# Combine name analysis with context analysis
if verb in ['is', 'has', 'should', 'can', 'will']:
description = [f"Validates if {subject} meets criteria | {context_description}"]
elif verb in ['get', 'fetch', 'retrieve']:
description = [f"Retrieves {subject} data | {context_description}"]
elif verb in ['set', 'update', 'modify']:
description = [f"Updates {subject} | {context_description}"]
elif verb in ['calc', 'compute', 'calculate']:
description = [f"Calculates {subject} | {context_description}"]
elif verb in ['handle', 'process']:
description = [f"Processes {subject} | {context_description}"]
elif verb in ['validate', 'verify']:
description = [f"Validates {subject} | {context_description}"]
elif verb in ['create', 'init', 'initialize']:
description = [f"Creates {subject} | {context_description}"]
elif verb in ['sort', 'order']:
description = [f"Sorts {subject} | {context_description}"]
else:
description = [context_description]
final_description = ' '.join(description)
# Add duplicate alert if needed, now with simplified line reporting
if func_name in duplicates:
first_line, count = duplicates[func_name]
final_description += f" **🔄 Duplicate Alert: Function appears {count} times (first occurrence: line {first_line})**"
functions.append((func_name, final_description))
return functions, len(content.split('\n'))
except Exception as e:
print(f"Error analyzing file {file_path}: {e}")
return [], 0
class RulesAnalyzer:
def __init__(self, project_path):
self.project_path = project_path
def analyze_project_for_rules(self):
"""Analyze project for .cursorrules generation"""
try:
project_info = {
"name": self.detect_project_name(),
"version": self.detect_version(),
"language": self.detect_main_language(),
"framework": self.detect_framework(),
"type": self.determine_project_type()
}
return project_info
except Exception as e:
logging.error(f"Error analyzing project for rules: {e}")
return self.get_default_project_info()
================================================
File: code_review.py
================================================
import os
import logging
import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
import re
from collections import defaultdict
import time
from difflib import SequenceMatcher
class CodeReviewGenerator:
def __init__(self, api_key: str):
self.api_key = api_key
def read_file_content(self, file_path: str) -> str:
"""Read content of a specific file"""
try:
with open(file_path, 'r') as f:
return f.read()
except Exception as e:
logging.error(f"Error reading file {file_path}: {str(e)}")
return ""
def get_relevant_files(self, project_path: str) -> List[str]:
"""Get list of relevant files for review"""
ignored = {'.git', 'node_modules', '.next', 'dist', 'build', 'coverage'}
files = []
for root, dirs, filenames in os.walk(project_path):
# Skip ignored directories
dirs[:] = [d for d in dirs if d not in ignored]
# Skip CursorFocus directory
if 'CursorFocus' in root.split(os.sep):
continue
for filename in filenames:
if filename.endswith(('.js', '.jsx', '.ts', '.tsx', '.py', '.css', '.scss')):
file_path = os.path.join(root, filename)
files.append(file_path)
return files
def analyze_code_structure(self, project_path: str, files: List[str]) -> Dict:
"""Analyze code structure and organization"""
structure_analysis = {
'technical': [],
'simple': [],
'files_by_type': defaultdict(list),
'potential_unused': []
}
# Analyze file organization
for file in files:
ext = os.path.splitext(file)[1]
rel_path = os.path.relpath(file, project_path)
structure_analysis['files_by_type'][ext].append(rel_path)
# Check for potentially unused files
if os.path.getsize(file) < 50: # Small files might be unused
structure_analysis['potential_unused'].append(rel_path)
return structure_analysis
def analyze_coding_standards(self, file_contents: Dict[str, str]) -> Dict:
"""Analyze coding standards and style"""
standards_analysis = {
'technical': [],
'simple': [],
'style_issues': defaultdict(list)
}
for file_path, content in file_contents.items():
lines = content.split('\n')
for i, line in enumerate(lines, 1):
# Check indentation
if line.strip() and line[0] != ' ' and not line.startswith(('import', 'from', 'class', 'def')):
standards_analysis['style_issues']['indentation'].append((file_path, i))
# Check line length
if len(line) > 100:
standards_analysis['style_issues']['line_length'].append((file_path, i))
# Check naming conventions
if re.search(r'[a-z][A-Z]', line): # Detect mixed case
standards_analysis['style_issues']['naming'].append((file_path, i))
return standards_analysis
def find_duplicate_code(self, file_contents: Dict[str, str]) -> Dict:
"""Detect duplicate code blocks"""
duplicates = {
'technical': [],
'simple': [],
'duplicates': []
}
# Simple duplicate function detection
function_patterns = {
'py': r'def\s+(\w+)',
'js': r'function\s+(\w+)|const\s+(\w+)\s*=\s*\(',
'ts': r'function\s+(\w+)|const\s+(\w+)\s*=\s*\('
}
functions = defaultdict(list)
for file_path, content in file_contents.items():
ext = os.path.splitext(file_path)[1][1:]
pattern = function_patterns.get(ext)
if pattern:
matches = re.finditer(pattern, content)
for match in matches:
func_name = match.group(1) or match.group(2)
functions[func_name].append(file_path)
for func_name, files in functions.items():
if len(files) > 1:
duplicates['duplicates'].append((func_name, files))
return duplicates
def check_security_issues(self, file_contents: Dict[str, str]) -> Dict:
"""Check for security issues"""
security_analysis = {
'technical': [],
'simple': [],
'issues': []
}
# Check for hardcoded secrets
secret_patterns = [
r'api[_-]key\s*=\s*["\']([^"\']+)["\']',
r'password\s*=\s*["\']([^"\']+)["\']',
r'secret\s*=\s*["\']([^"\']+)["\']',
r'token\s*=\s*["\']([^"\']+)["\']'
]
for file_path, content in file_contents.items():
for pattern in secret_patterns:
matches = re.finditer(pattern, content, re.IGNORECASE)
for match in matches:
security_analysis['issues'].append({
'file': file_path,
'type': 'hardcoded_secret',
'pattern': pattern
})
return security_analysis
def analyze_error_handling(self, file_contents: Dict[str, str]) -> Dict:
"""Analyze error handling practices"""
error_analysis = {
'technical': [],
'simple': [],
'missing_error_handling': []
}
for file_path, content in file_contents.items():
if file_path.endswith(('.py', '.js', '.ts')):
# Check for functions without try-catch
function_blocks = re.finditer(r'(async\s+)?(?:function|def)\s+\w+\s*\([^)]*\)\s*{?[^}]*$', content, re.MULTILINE)
for match in function_blocks:
block_content = content[match.start():match.end()]
if 'try' not in block_content and 'catch' not in block_content:
error_analysis['missing_error_handling'].append(file_path)
return error_analysis
def check_documentation(self, file_contents: Dict[str, str]) -> Dict:
"""Check documentation coverage"""
doc_analysis = {
'technical': [],
'simple': [],
'missing_docs': []
}
for file_path, content in file_contents.items():
if file_path.endswith('.py'):
# Check for missing docstrings
functions = re.finditer(r'def\s+\w+\s*\([^)]*\):', content)
for match in functions:
pos = match.end()
next_lines = content[pos:pos+100]
if '"""' not in next_lines and "'''" not in next_lines:
doc_analysis['missing_docs'].append(file_path)
return doc_analysis
def generate_review_prompt(self, focus_content: str, analysis_results: Dict) -> str:
"""Generate the prompt for AI review with analysis results"""
prompt = """As an expert code reviewer, analyze the following codebase and provide a comprehensive review.
Project Overview from Focus.md:
{focus_content}
Analysis Results:
{analysis_results}
Please provide a detailed review in two sections:
1. Technical Review
- Code structure and organization assessment
- Coding standards compliance
- Security vulnerabilities
- Performance considerations
- Error handling practices
- Documentation coverage
- Duplicate code analysis
- Dependency management
- Testing coverage
2. Simple Explanation
- Overall project organization
- Code clarity and maintainability
- Security concerns in plain language
- Suggestions for improvement
- Priority action items
For each section, provide specific examples and actionable recommendations.
"""
return prompt.format(
focus_content=focus_content,
analysis_results=str(analysis_results)
)
def analyze_file(self, file_path: str, content: str, all_files_content: Dict[str, str]) -> Dict:
"""Analyze a single file and provide simple explanation"""
# Store the current file path for use in _extract_functions
self.current_file = file_path
# Get file type
file_type = os.path.splitext(file_path)[1][1:]
analysis = {
'explanation': self._generate_simple_explanation(file_path, content),
'similar_files': [],
'functionality_alerts': []
}
# Get line count
lines = content.splitlines()
line_count = len(lines)
if line_count > 200: # Even lower threshold for length warnings
analysis['functionality_alerts'].append({
'type': 'length_warning',
'details': f'File exceeds recommended length of 200 lines',
'count': line_count
})
# Extract and analyze functions
functions = self._extract_functions(content, file_type)
# Find duplicate functions with line numbers
if file_type in ['js', 'jsx', 'ts', 'tsx', 'py']:
function_counts = defaultdict(list)
# First pass: collect all functions
for other_path, other_content in all_files_content.items():
if other_path != file_path:
other_functions = self._extract_functions(other_content, os.path.splitext(other_path)[1][1:])
for func in other_functions:
function_counts[func['name']].append(other_path)
# Second pass: check for duplicates
for func in functions:
if func['name'] in function_counts:
# Find the line number
for i, line in enumerate(lines, 1):
if func['name'] in line and ('function' in line or 'def' in line or '=>' in line):
analysis['functionality_alerts'].append({
'type': 'duplicate_functionality',
'details': f"Function '{func['name']}' is duplicated",
'count': len(function_counts[func['name']]) + 1,
'line': i,
'locations': function_counts[func['name']]
})
break
# Find similar file content
for other_path, other_content in all_files_content.items():
if other_path != file_path:
similarity = self._calculate_similarity(
self._clean_content(content),
self._clean_content(other_content)
)
if similarity > 0.7: # 70% similarity threshold
analysis['functionality_alerts'].append({
'type': 'similar_content',
'details': f"File has {int(similarity * 100)}% similar content with {os.path.basename(other_path)}",
'file': other_path
})
else:
# Add to related files if there's some similarity
if similarity > 0.3: # 30% similarity threshold for related files
analysis['similar_files'].append({
'file': other_path
})
return analysis
def _are_names_similar(self, name1: str, name2: str) -> bool:
"""Check if two file names are similar"""
# Remove extension and common prefixes/suffixes
name1 = os.path.splitext(name1)[0]
name2 = os.path.splitext(name2)[0]
# Remove common words
common_words = ['test', 'utils', 'helper', 'component', 'page', 'api']
for word in common_words:
name1 = name1.replace(word, '')
name2 = name2.replace(word, '')
# Compare the core names
return name1 and name2 and (name1 in name2 or name2 in name1)
def _have_similar_content(self, content1: str, content2: str) -> bool:
"""Check if two files have similar content"""
# Remove comments and whitespace
content1 = self._clean_content(content1)
content2 = self._clean_content(content2)
# If either content is empty, return False
if not content1 or not content2:
return False
# Calculate similarity ratio
similarity = SequenceMatcher(None, content1, content2).ratio()
return similarity > 0.7 # Files are considered similar if they're 70% identical
def _clean_content(self, content: str) -> str:
"""Remove comments and whitespace from content"""
# Remove single-line comments
content = re.sub(r'//.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'#.*$', '', content, flags=re.MULTILINE)
# Remove multi-line comments
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
# Remove empty lines and whitespace
return '\n'.join(line.strip() for line in content.splitlines() if line.strip())
def _generate_function_description(self, func_name: str, func_body: str, file_type: str) -> str:
"""Generate a simple description of what a function does"""
# Special case components
special_components = {
'RootLayout': "The main layout component that wraps all pages",
'Layout': "Provides the structure and layout for pages",
'Loading': "Shows a loading state while content is being prepared",
'NotFound': "Displays a friendly 404 error page when content isn't found",
'Error': "Shows an error message when something goes wrong",
'Page': "The main content component for this route",
'Header': "The top section of the page with navigation and branding",
'Footer': "The bottom section of the page with additional links and info",
'Sidebar': "A side panel with navigation or additional content",
'Navigation': "Helps users move between different parts of the site",
'Modal': "A popup window that appears over the main content",
'Dialog': "A popup window for user interactions or messages",
'Form': "Collects user input through various fields",
'Button': "A clickable element that triggers actions",
'Input': "Allows users to enter text or data",
'Select': "Lets users choose from a list of options",
'Card': "Displays content in a card-style container",
'List': "Shows multiple items in a structured way",
'Table': "Displays data in rows and columns",
'Menu': "Shows a list of options or actions",
'Dropdown': "Reveals additional options when clicked",
'Tabs': "Organizes content into different sections",
'Alert': "Shows important messages or notifications",
'Toast': "Displays temporary notification messages",
'Tooltip': "Shows helpful text when hovering over elements",
'Badge': "Displays a small count or status indicator",
'Avatar': "Shows a user's profile picture or icon",
'Icon': "Displays a small symbolic image",
'Spinner': "Shows an animated loading indicator",
'Progress': "Indicates progress of an operation",
'Skeleton': "Shows a placeholder while content loads"
}
# Check for special case components first
if func_name in special_components:
return special_components[func_name]
# Handle Next.js special files
if hasattr(self, 'current_file'):
file_name = os.path.basename(self.current_file)
base_name = os.path.splitext(file_name)[0]
# Map file names to component descriptions
next_components = {
'layout': ('RootLayout', "The main layout component that wraps all pages"),
'page': ('Page', "The main content component for this route"),
'loading': ('Loading', "Shows a loading state while content is being prepared"),
'not-found': ('NotFound', "Displays a friendly 404 error page when content isn't found"),
'error': ('Error', "Shows an error message when something goes wrong"),
'middleware': ('middleware', "Handles request middleware for authentication and routing")
}
if base_name in next_components:
component_name, description = next_components[base_name]
return description
# Extract parameters if they exist
params = re.findall(r'\((.*?)\)', func_body.split('\n')[0])
param_list = []
if params:
# Clean up parameters
param_text = params[0].strip()
if param_text and param_text != '()':
param_list = [p.strip().split(':')[0] for p in param_text.split(',')]
# Check if it's a React component
if file_type in ['tsx', 'jsx'] and func_name[0].isupper():
# Look for common UI patterns in the name
component_types = {
'button': "a clickable button",
'list': "a list of items",
'form': "a form for user input",
'modal': "a popup window",
'dialog': "a popup window",
'card': "a card-style container",
'header': "header content",
'footer': "footer content",
'nav': "navigation elements",
'menu': "a menu of options",
'input': "an input field",
'select': "a dropdown selection",
'table': "a data table",
'grid': "a grid layout",
'container': "a content container",
'wrapper': "a wrapper component",
'provider': "provides data or functionality",
'view': "a view component",
'panel': "a panel of content",
'section': "a section of content"
}
description = "A component that shows "
found_type = False
# Check component name against known types
for type_key, type_desc in component_types.items():
if type_key in func_name.lower():
description += type_desc
found_type = True
break
if not found_type:
# Convert PascalCase to spaces for a readable name
readable_name = ' '.join(re.findall('[A-Z][^A-Z]*', func_name)).lower()
description += f"{readable_name}"
# Add parameter context if available
if param_list:
description += f" (uses: {', '.join(param_list)})"
return description
# Common React/Next.js patterns
react_patterns = {
r'^use[A-Z]': "A custom hook that ",
r'^handle[A-Z]': "Handles when ",
r'^on[A-Z]': "Responds when ",
r'^get[A-Z]': "Gets or retrieves ",
r'^set[A-Z]': "Updates or changes ",
r'^is[A-Z]': "Checks if ",
r'^has[A-Z]': "Checks if there is ",
r'^format[A-Z]': "Formats or arranges ",
r'^validate[A-Z]': "Checks if valid ",
r'^parse[A-Z]': "Processes and understands ",
r'^render[A-Z]': "Shows or displays ",
r'^create[A-Z]': "Creates or makes new ",
r'^update[A-Z]': "Updates or modifies ",
r'^delete[A-Z]': "Removes or deletes ",
r'^fetch[A-Z]': "Gets data from ",
r'^load[A-Z]': "Loads or prepares ",
r'^save[A-Z]': "Saves or stores ",
r'^convert[A-Z]': "Converts or changes ",
r'^calculate[A-Z]': "Calculates or computes ",
r'^filter[A-Z]': "Filters or selects ",
r'^sort[A-Z]': "Arranges or orders ",
r'^search[A-Z]': "Searches for ",
r'^find[A-Z]': "Finds or locates ",
r'^toggle[A-Z]': "Switches between ",
r'^show[A-Z]': "Displays or reveals ",
r'^hide[A-Z]': "Hides or removes from view ",
r'^open[A-Z]': "Opens or shows ",
r'^close[A-Z]': "Closes or hides ",
r'^enable[A-Z]': "Turns on or activates ",
r'^disable[A-Z]': "Turns off or deactivates ",
r'^add[A-Z]': "Adds or includes ",
r'^remove[A-Z]': "Removes or takes away ",
r'^clear[A-Z]': "Clears or resets ",
r'^reset[A-Z]': "Resets or restores "
}
# Check for common function patterns
for pattern, desc_prefix in react_patterns.items():
if re.match(pattern, func_name):
# Convert camelCase to spaces after the prefix
name_parts = re.sub(r'([A-Z])', r' \1', func_name).split()
action_part = ' '.join(name_parts[1:]).lower()
description = desc_prefix + action_part
# Add parameter context if available
if param_list:
description += f" (uses: {', '.join(param_list)})"
return description
# If no pattern matched, create a basic description
# Convert camelCase/PascalCase to spaces
readable_name = re.sub(r'([A-Z])', r' \1', func_name).lower()
description = f"Handles {readable_name}"
# Add parameter context if available
if param_list:
description += f" (uses: {', '.join(param_list)})"
return description
def _extract_functions(self, content: str, file_type: str) -> List[Dict[str, Any]]:
"""Extract functions and their details from the file content"""
functions = []
# Skip binary or empty files
if not content or is_binary_file(content):
return functions
try:
# Different patterns for different file types
if file_type in ('ts', 'tsx', 'js', 'jsx'):
# React component pattern
component_pattern = r'(?:export\s+(?:default\s+)?)?(?:const|class|function)\s+([A-Z][a-zA-Z0-9]*)\s*(?:=|\{|\()'
for match in re.finditer(component_pattern, content):
functions.append({
'name': match.group(1),
'type': 'component',
'line': content.count('\n', 0, match.start()) + 1
})
# Hook pattern
hook_pattern = r'(?:export\s+(?:default\s+)?)?(?:const|function)\s+(use[A-Z][a-zA-Z0-9]*)\s*(?:=|\()'
for match in re.finditer(hook_pattern, content):
functions.append({
'name': match.group(1),
'type': 'hook',
'line': content.count('\n', 0, match.start()) + 1
})
# Regular function pattern
function_pattern = r'(?:export\s+(?:default\s+)?)?(?:async\s+)?(?:function|const)\s+([a-z][a-zA-Z0-9]*)\s*(?:=|\()'
for match in re.finditer(function_pattern, content):
functions.append({
'name': match.group(1),
'type': 'function',
'line': content.count('\n', 0, match.start()) + 1
})
elif file_type == 'py':
# Python function pattern
function_pattern = r'def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
for match in re.finditer(function_pattern, content):
functions.append({
'name': match.group(1),
'type': 'function',
'line': content.count('\n', 0, match.start()) + 1
})
# Python class pattern
class_pattern = r'class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\([^)]*\))?\s*:'
for match in re.finditer(class_pattern, content):
functions.append({
'name': match.group(1),
'type': 'class',
'line': content.count('\n', 0, match.start()) + 1
})
except Exception as e:
logging.debug(f"Error extracting functions: {str(e)}")
return functions
def _extract_function_body(self, content: str, file_type: str) -> str:
"""Extract function body based on file type"""
if file_type == 'py':
# Python: Find indented block
lines = content.splitlines()
body = []
if not lines:
return ""
# Get first line's indentation
first_line = lines[0]
base_indent = len(first_line) - len(first_line.lstrip())
for line in lines:
if line.strip() and len(line) - len(line.lstrip()) <= base_indent:
break
body.append(line)
return '\n'.join(body)
else:
# JavaScript/TypeScript: Find block between { }
stack = []
for i, char in enumerate(content):
if char == '{':
stack.append(i)
elif char == '}':
if stack:
start = stack.pop()
if not stack: # We found the matching outer brace
return content[start:i+1]
return ""
def _find_duplicate_functions(self, functions1: List[Dict], functions2: List[Dict]) -> List[str]:
"""Find duplicate functions between two sets by comparing implementation and context"""
duplicates = []
# Common method names to ignore (these are expected to appear multiple times)
ignore_methods = {
# React/Next.js patterns
'getLayout', 'getInitialProps', 'getStaticProps', 'getServerSideProps',
'layout', 'loading', 'error', 'notFound',
# Common React hooks
'useEffect', 'useState', 'useMemo', 'useCallback',
# Common utility names
'init', 'setup', 'configure', 'getConfig', 'getData',
# Common class methods
'__init__', '__str__', '__repr__', '__len__', 'toString',
# Testing functions
'setUp', 'tearDown', 'beforeEach', 'afterEach',
}
for func1 in functions1:
for func2 in functions2:
# Skip ignored method names
if func1['name'] in ignore_methods:
continue
# Only compare functions with same name
if func1['name'] == func2['name']:
# Clean and normalize the function bodies
body1 = self._normalize_function_body(func1['body'])
body2 = self._normalize_function_body(func2['body'])
# Calculate similarity score
similarity = self._calculate_similarity(body1, body2)
# If bodies are very similar (80% or more), it's likely a real duplication
if similarity >= 0.8:
duplicates.append({
'name': func1['name'],
'similarity': similarity,
'reason': 'Implementation is nearly identical'
})
# If bodies are somewhat similar (60-80%), check the context
elif similarity >= 0.6:
context_similarity = self._check_function_context(func1, func2)
if context_similarity >= 0.7:
duplicates.append({
'name': func1['name'],
'similarity': similarity,
'reason': 'Similar implementation and usage context'
})
return duplicates
def _normalize_function_body(self, body: str) -> str:
"""Normalize function body for comparison by removing noise"""
# Remove comments
body = re.sub(r'//.*$', '', body, flags=re.MULTILINE)
body = re.sub(r'/\*.*?\*/', '', body, flags=re.DOTALL)
body = re.sub(r'#.*$', '', body, flags=re.MULTILINE)
# Remove string literals
body = re.sub(r'"[^"]*"', '""', body)
body = re.sub(r"'[^']*'", "''", body)
# Remove whitespace and normalize line endings
body = '\n'.join(line.strip() for line in body.splitlines() if line.strip())
# Remove variable names but keep structure
body = re.sub(r'\b\w+\s*=', '=', body)
body = re.sub(r'\bconst\s+\w+\s*=', 'const=', body)
body = re.sub(r'\blet\s+\w+\s*=', 'let=', body)
body = re.sub(r'\bvar\s+\w+\s*=', 'var=', body)
return body
def _calculate_similarity(self, text1: str, text2: str) -> float:
"""Calculate similarity ratio between two texts"""
if not text1 or not text2:
return 0.0
return SequenceMatcher(None, text1, text2).ratio()
def _check_function_context(self, func1: Dict, func2: Dict) -> float:
"""Check if functions are used in similar contexts"""
# Extract function calls and dependencies
calls1 = set(re.findall(r'\b\w+\(', func1['body']))
calls2 = set(re.findall(r'\b\w+\(', func2['body']))
# Extract variable usage patterns
vars1 = set(re.findall(r'\b\w+\s*=|\b\w+\s*\+=|\b\w+\s*-=', func1['body']))
vars2 = set(re.findall(r'\b\w+\s*=|\b\w+\s*\+=|\b\w+\s*-=', func2['body']))
# Calculate similarity of usage patterns
calls_similarity = len(calls1.intersection(calls2)) / max(len(calls1), len(calls2), 1)
vars_similarity = len(vars1.intersection(vars2)) / max(len(vars1), len(vars2), 1)
return (calls_similarity + vars_similarity) / 2
def _generate_simple_explanation(self, file_path: str, content: str) -> str:
"""Generate an intelligent, context-aware explanation of what the file does"""
# Get file extension and name
ext = os.path.splitext(file_path)[1][1:].lower()
name = os.path.basename(file_path)
# Extract key information
functions = self._extract_functions(content, ext) or []
imports = self._extract_imports(content)
exports = self._extract_exports(content)
# Initialize description components
purpose = []
features = []
integrations = []
# Analyze file type and location
if '/api/' in file_path:
endpoint = file_path.split('/api/')[-1].split('/')[0]
purpose.append(f"This API endpoint handles {endpoint} functionality")
if 'route' in name.lower():
features.append("implements routing logic")
elif '/components/' in file_path:
component_names = [f['name'] for f in functions if f['name'][0].isupper()]
if component_names:
purpose.append(f"This React component implements {', '.join(component_names)}")
if any(f['name'].startswith('use') for f in functions):
features.append("includes custom hooks")
elif '/services/' in file_path or 'service' in name.lower():
service_type = next((word for word in file_path.split('/') if 'service' in word.lower()), 'service')
purpose.append(f"This service module provides {service_type.replace('Service', '').replace('service', '')} functionality")
elif '/utils/' in file_path or 'util' in name.lower() or 'helper' in name.lower():
purpose.append("This utility module provides helper functions")
if functions:
features.append(f"includes {len(functions)} utility functions")
elif '/hooks/' in file_path or any(f['name'].startswith('use') for f in functions):
hook_names = [f['name'] for f in functions if f['name'].startswith('use')]
if hook_names:
purpose.append(f"This custom hook module implements {', '.join(hook_names)}")
elif '/types/' in file_path or file_path.endswith('.d.ts'):
purpose.append("This type definition file declares interfaces and types")
if exports:
features.append(f"defines {len(exports)} types/interfaces")
elif '/tests/' in file_path or 'test' in name.lower() or 'spec' in name.lower():
purpose.append("This test file verifies functionality")
if functions:
features.append(f"contains {len(functions)} test cases")
elif file_path.endswith(('.css', '.scss', '.less')):
purpose.append("This style file defines visual appearance and layout")
elif 'config' in name.lower() or file_path.endswith(('.json', '.env')):
purpose.append("This configuration file manages project settings")
else:
# Default case - analyze based on content
if functions:
main_functions = [f['name'] for f in functions[:3]]
purpose.append("This module implements application logic")
features.append(f"key functions: {', '.join(main_functions)}")
# Add integration details
if imports:
major_deps = [dep for dep in imports.keys() if not dep.startswith('.')][:2]
if major_deps:
integrations.append(f"integrates with {' and '.join(major_deps)}")
# Combine all parts
description = []
if purpose:
description.extend(purpose)
if features:
description.append(f"It {', '.join(features)}")
if integrations:
description.extend(integrations)
return '. '.join(description) + '.'
def _extract_imports(self, content: str) -> Dict[str, set]:
"""Extract imports from file content"""
imports = {}
# Match ES6/TypeScript imports
es6_pattern = r'import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+[\'"]([^\'"]+)[\'"]'
for match in re.finditer(es6_pattern, content):
module = match.group(1)
imports[module] = set()
# Match Python imports
python_pattern = r'(?:from\s+([^\s]+)\s+import|import\s+([^\s]+))'
for match in re.finditer(python_pattern, content):
module = match.group(1) or match.group(2)
imports[module] = set()
return imports
def _extract_exports(self, content: str) -> List[str]:
"""Extract exports from file content"""
exports = []
# Match ES6/TypeScript exports
es6_patterns = [
r'export\s+(?:default\s+)?(?:class|interface|type|const|let|var|function)\s+(\w+)',
r'export\s+{\s*([^}]+)\s*}'
]
for pattern in es6_patterns:
for match in re.finditer(pattern, content):
if ',' in match.group(1):
exports.extend(name.strip() for name in match.group(1).split(','))
else:
exports.append(match.group(1).strip())
# Match Python exports
python_pattern = r'__all__\s*=\s*\[([^\]]+)\]'
for match in re.finditer(python_pattern, content):
exports.extend(name.strip().strip("'\"") for name in match.group(1).split(','))
return exports
def _analyze_functionality(self, file_path: str, content: str, all_files_content: Dict[str, str]) -> List[Dict]:
"""Analyze functionality duplication and import/export mismatches"""
alerts = []
# Extract imports and exports from current file
current_imports = self._extract_imports(content)
current_exports = self._extract_exports(content)
# Look for functionality duplication
for other_path, other_content in all_files_content.items():
if other_path == file_path:
continue
other_imports = self._extract_imports(other_content)
other_exports = self._extract_exports(other_content)
# Get relative paths for comparison
rel_path = os.path.relpath(file_path)
other_rel_path = os.path.relpath(other_path)
# Extract and compare functions
current_functions = self._extract_functions(content, os.path.splitext(file_path)[1][1:])
other_functions = self._extract_functions(other_content, os.path.splitext(other_path)[1][1:])
duplicates = self._find_duplicate_functions(current_functions, other_functions)
if duplicates:
details = []
for dup in duplicates:
details.append(f"{dup['name']} ({int(dup['similarity']*100)}% similar - {dup['reason']})")
alerts.append({
'type': 'duplicate_functionality',
'file': other_path,
'details': f"These functions have similar implementations: {', '.join(details)}"
})
# Check for circular dependencies
if any(p in other_imports for p in [rel_path, os.path.splitext(rel_path)[0]]) and \
any(p in current_imports for p in [other_rel_path, os.path.splitext(other_rel_path)[0]]):
alerts.append({
'type': 'circular_dependency',
'file': other_path,
'details': "These files import each other, which could cause problems"
})
# Check for mismatched imports
for source, items in current_imports.items():
if source in [other_rel_path, os.path.splitext(other_rel_path)[0]]:
missing_exports = items - other_exports - {'*'}
if missing_exports:
alerts.append({
'type': 'import_mismatch',
'file': other_path,
'details': f"Trying to import {', '.join(sorted(missing_exports))} but they're not exported from the file"
})
return alerts
def generate_review(self, project_path: str) -> str:
"""Generate a file-by-file code review"""
try:
files = self.get_relevant_files(project_path)
if not files:
return "No relevant files found for analysis."
# Filter out any CursorFocus files
cursorfocus_path = os.path.join(project_path, 'CursorFocus')
files = [f for f in files if not f.startswith(cursorfocus_path)]
file_contents = {f: self.read_file_content(f) for f in files}
file_analyses = {}
for file_path, content in file_contents.items():
file_analyses[file_path] = self.analyze_file(file_path, content, file_contents)
# Format the review
formatted_review = "# Code Review Report\n\n"
# Group files by directory
by_directory = defaultdict(list)
for file_path in files:
dir_path = os.path.dirname(file_path)
by_directory[dir_path].append(file_path)
# Generate review for each directory
for dir_path, dir_files in sorted(by_directory.items()):
rel_dir = os.path.relpath(dir_path, project_path)
formatted_review += f"\n## 📁 {rel_dir}\n"
formatted_review += f"{self._get_directory_purpose(rel_dir)}\n\n"
for file_path in sorted(dir_files):
rel_path = os.path.relpath(file_path, project_path)
analysis = file_analyses[file_path]
content = file_contents[file_path]
line_count = len(content.splitlines())
# Add empty line before each file entry
formatted_review += "\n"
# File header with line count
formatted_review += f"`/{rel_path}` ({line_count} lines)\n"
# Description
formatted_review += "**What this file does:**\n"
formatted_review += f"{analysis['explanation']}\n"
# Alerts
if analysis['functionality_alerts']:
formatted_review += "**⚠️ Alerts:**\n"
for alert in analysis['functionality_alerts']:
if alert['type'] == 'length_warning':
formatted_review += f"- 📏 {alert['details']} (Current: {alert['count']} lines)\n"
elif alert['type'] == 'duplicate_functionality':
locations = ', '.join(f'`{os.path.relpath(loc, project_path)}`' for loc in alert.get('locations', []))
formatted_review += f"- 🔄 {alert['details']} (Found in: {locations})\n"
elif alert['type'] == 'similar_content':
formatted_review += f"- 👯 {alert['details']}\n"
# Key Functions
functions = self._extract_functions(content, os.path.splitext(file_path)[1][1:])
if functions:
formatted_review += "**Key Functions:**\n"
for func in functions:
description = self._generate_function_description(func['name'], '', os.path.splitext(file_path)[1][1:])
formatted_review += f"<{func['name']}>: {description}\n"
# Related files
if analysis['similar_files']:
formatted_review += "**Related files:**\n"
for similar in analysis['similar_files']:
formatted_review += f"- Works with `{os.path.relpath(similar['file'], project_path)}`\n"
formatted_review += "---\n\n\n" # Added an extra newline here
formatted_review += f"\n## Review Date\n{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
return formatted_review
except Exception as e:
logging.error(f"Error in review generation: {str(e)}", exc_info=True)
return f"Error generating review: {str(e)}"
def _get_directory_purpose(self, dir_path: str) -> str:
"""Get a simple explanation of what a directory is for"""
# Generic directory purposes
common_dirs = {
'.': "Project root directory",
'src': "Source code directory",
'components': "Reusable components",
'utils': "Utility functions",
'styles': "Style definitions",
'api': "API-related code",
'public': "Public assets",
'types': "Type definitions",
'tests': "Test files",
'docs': "Documentation",
'config': "Configuration files",
'lib': "Library code",
'assets': "Static assets",
'scripts': "Build and utility scripts"
}
# Handle nested paths
parts = dir_path.split(os.sep)
if len(parts) > 1:
if parts[0] in common_dirs:
base_purpose = common_dirs[parts[0]]
sub_path = '/'.join(parts[1:])
return f"{base_purpose} - {sub_path}"
return common_dirs.get(dir_path, f"Directory: {dir_path}")
def is_binary_file(content: str) -> bool:
"""Check if file content appears to be binary"""
try:
content.encode('utf-8')
return False
except UnicodeError:
return True
def main():
"""Main function to generate code review."""
logging.basicConfig(level=logging.INFO)
# Load environment variables
from dotenv import load_dotenv
cursorfocus_env = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(cursorfocus_env)
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
logging.error("GEMINI_API_KEY not found in environment variables")
return
# Use parent directory as project path
project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Generate review
reviewer = CodeReviewGenerator(api_key)
review_content = reviewer.generate_review(project_path)
# Save review in project root
output_file = os.path.join(project_path, 'CodeReview.md')
try:
with open(output_file, 'w') as f:
f.write(review_content)
print(f"Code review saved to {output_file}")
except Exception as e:
logging.error(f"Error saving review: {str(e)}")
if __name__ == '__main__':
main()
================================================
File: config.example.json
================================================
{
"projects": [
{
"name": "Example Project",
"project_path": "/path/to/your/project",
"type": "node_js",
"custom_rules": {
"max_file_size": 500,
"ignored_patterns": [
"*.test.ts",
"*.spec.ts",
"CursorFocus/*"
]
},
"watch": true
}
],
"update_interval": 60,
"max_depth": 3,
"ignored_directories": [
"__pycache__",
"node_modules",
"venv",
".git",
".idea",
".vscode",
"dist",
"build",
"CursorFocus"
],
"ignored_files": [
".DS_Store",
"*.pyc",
"*.pyo"
],
"binary_extensions": [
".png",
".jpg",
".jpeg",
".gif",
".ico",
".pdf",
".exe",
".bin"
],
"file_length_standards": {
".js": 300,
".jsx": 250,
".ts": 300,
".tsx": 250,
".py": 400,
".css": 400,
".scss": 400,
".less": 400,
".sass": 400,
".html": 300,
".vue": 250,
".svelte": 250,
".json": 100,
".yaml": 100,
".yml": 100,
".toml": 100,
".md": 500,
".rst": 500,
"default": 300
},
"file_length_thresholds": {
"warning": 1.0,
"critical": 1.5,
"severe": 2.0
},
"project_types": {
"chrome_extension": {
"indicators": [
"manifest.json"
],
"required_files": [],
"description": "Chrome Extension"
},
"node_js": {
"indicators": [
"package.json"
],
"required_files": [],
"description": "Node.js Project"
},
"python": {
"indicators": [
"setup.py",
"pyproject.toml"
],
"required_files": [],
"description": "Python Project"
},
"react": {
"indicators": [],
"required_files": [
"src/App.js",
"src/index.js"
],
"description": "React Application"
}
}
}
================================================
File: config.py
================================================
import os
import json
def load_config():
"""Load configuration from config.json."""
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')
if os.path.exists(config_path):
with open(config_path, 'r') as f:
return json.load(f)
return get_default_config()
except Exception as e:
print(f"Error loading config: {e}")
return None
def get_default_config():
"""Get default configuration settings."""
return {
"project_path": "",
"update_interval": 60,
"max_depth": 3,
"ignored_directories": [
"__pycache__",
"node_modules",
"venv",
".git",
".idea",
".vscode",
"dist",
"build",
"coverage"
],
"ignored_files": [
".DS_Store",
"Thumbs.db",
"*.pyc",
"*.pyo",
"package-lock.json",
"yarn.lock"
],
"binary_extensions": [
".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".exe", ".bin"
],
"file_length_standards": {
".js": 300,
".jsx": 250,
".ts": 300,
".tsx": 250,
".py": 400,
".css": 400,
".scss": 400,
".less": 400,
".sass": 400,
".html": 300,
".vue": 250,
".svelte": 250,
".json": 100,
".yaml": 100,
".yml": 100,
".toml": 100,
".md": 500,
".rst": 500,
"default": 300
}
}
# Load configuration once at module level
_config = load_config()
# Binary file extensions that should be ignored
BINARY_EXTENSIONS = set(_config.get('binary_extensions', []))
# Documentation and text files that shouldn't be analyzed for functions
NON_CODE_EXTENSIONS = {
'.md', '.txt', '.log', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
'.conf', '.config', '.markdown', '.rst', '.rdoc', '.csv', '.tsv'
}
# Extensions that should be analyzed for code
CODE_EXTENSIONS = {
'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.cpp', '.c', '.h',
'.hpp', '.cs', '.go', '.rb', '.php'
}
# Regex patterns for function detection
FUNCTION_PATTERNS = {
'standard': r'(?:^|\s+)(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?function)',
'arrow': r'(?:^|\s+)(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[^=])\s*=>',
'method': r'\b(\w+)\s*:\s*(?:async\s*)?function',
'class_method': r'(?:^|\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*{',
'object_property': r'(\w+)\s*:\s*(?:\([^)]*\)|[^=])\s*=>'
}
# Keywords that should not be treated as function names
IGNORED_KEYWORDS = {
'if', 'switch', 'while', 'for', 'catch', 'finally', 'else', 'return',
'break', 'continue', 'case', 'default', 'to', 'from', 'import', 'as',
'try', 'except', 'raise', 'with', 'async', 'await', 'yield', 'assert',
'pass', 'del', 'print', 'in', 'is', 'not', 'and', 'or', 'lambda',
'global', 'nonlocal', 'class', 'def', 'n', 'lines', 'directly'
}
# Names of files and directories that should be ignored
IGNORED_NAMES = set(_config.get('ignored_directories', []))
FILE_LENGTH_STANDARDS = _config.get('file_length_standards', {})
def get_file_length_limit(file_path):
"""Get the recommended line limit for a given file type."""
ext = os.path.splitext(file_path)[1].lower()
return FILE_LENGTH_STANDARDS.get(ext, FILE_LENGTH_STANDARDS.get('default', 300))
================================================
File: content_generator.py
================================================
import os
from datetime import datetime
from analyzers import analyze_file_content, should_ignore_file, is_binary_file
from project_detector import detect_project_type, get_project_description, get_file_type_info
from config import get_file_length_limit, load_config
class ProjectMetrics:
def __init__(self):
self.total_files = 0
self.total_lines = 0
self.files_by_type = {}
self.lines_by_type = {}
self.alerts = {
'warning': 0,
'critical': 0,
'severe': 0
}
self.duplicate_functions = 0
def get_file_length_alert(line_count, limit, thresholds):
"""Get alert level based on file length and thresholds."""
ratio = line_count / limit
if ratio >= thresholds.get('severe', 2.0):
return 'severe', f"🚨 Critical-Length Alert: File is more than {int(thresholds['severe']*100)}% of recommended length"
elif ratio >= thresholds.get('critical', 1.5):
return 'critical', f"⚠️ High-Length Alert: File is more than {int(thresholds['critical']*100)}% of recommended length"
elif ratio >= thresholds.get('warning', 1.0):
return 'warning', f"📄 Length Alert: File exceeds recommended length"
return None, None
def generate_focus_content(project_path, config):
"""Generate the Focus file content."""
metrics = ProjectMetrics()
thresholds = config.get('file_length_thresholds', {
'warning': 1.0,
'critical': 1.5,
'severe': 2.0
})
project_type = detect_project_type(project_path)
project_info = get_project_description(project_path)
content = [
f"# Project Focus: {project_info['name']}",
"",
f"**Current Goal:** {project_info['description']}",
"",
"**Key Components:**"
]
# Add directory structure
structure = get_directory_structure(project_path, config['max_depth'])
content.extend(structure_to_tree(structure))
content.extend([
"",
"**Project Context:**",
f"Type: {project_info['key_features'][1].replace('Type: ', '')}",
f"Target Users: Users of {project_info['name']}",
f"Main Functionality: {project_info['description']}",
"Key Requirements:",
*[f"- {feature}" for feature in project_info['key_features']],
"",
"**Development Guidelines:**",
"- Keep code modular and reusable",
"- Follow best practices for the project type",
"- Maintain clean separation of concerns",
"",
"# File Analysis"
])
# Analyze each file
first_file = True
for root, _, files in os.walk(project_path):
if any(ignored in root.split(os.path.sep) for ignored in config['ignored_directories']):
continue
for file in files:
if any(file.endswith(ignored.replace('*', '')) for ignored in config['ignored_files']):
continue
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, project_path)
if is_binary_file(file_path):
continue
metrics.total_files += 1
functions, line_count = analyze_file_content(file_path)
if functions or line_count > 0:
if not first_file:
content.append("")
else:
first_file = False
file_type, file_desc = get_file_type_info(file)
content.append(f"`{rel_path}` ({line_count} lines)")
content.append(f"**Main Responsibilities:** {file_desc}")
# Update metrics
ext = os.path.splitext(file)[1].lower()
metrics.files_by_type[ext] = metrics.files_by_type.get(ext, 0) + 1
metrics.lines_by_type[ext] = metrics.lines_by_type.get(ext, 0) + line_count
metrics.total_lines += line_count
if functions:
content.append("**Key Functions:**")
for func_name, description in functions:
content.append(f"<{func_name}>: {description}")
if "Duplicate Alert" in description:
metrics.duplicate_functions += 1
# Get file-specific length limit and check thresholds
length_limit = get_file_length_limit(file_path)
alert_level, alert_message = get_file_length_alert(line_count, length_limit, thresholds)
if alert_level:
metrics.alerts[alert_level] += 1
content.append(f"**{alert_message} ({line_count} lines vs. recommended {length_limit})**")
# Add metrics summary
content.extend([
"",
"# Project Metrics Summary",
f"Total Files: {metrics.total_files}",
f"Total Lines: {metrics.total_lines:,}",
"",
"**Files by Type:**",
*[f"- {ext}: {count} files ({metrics.lines_by_type[ext]:,} lines)"
for ext, count in sorted(metrics.files_by_type.items())],
"",
"**Code Quality Alerts:**",
f"- 🚨 Severe Length Issues: {metrics.alerts['severe']} files",
f"- ⚠️ Critical Length Issues: {metrics.alerts['critical']} files",
f"- 📄 Length Warnings: {metrics.alerts['warning']} files",
f"- 🔄 Duplicate Functions: {metrics.duplicate_functions}",
"",
f"Last updated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}"
])
return '\n'.join(content)
def get_directory_structure(project_path, max_depth=3, current_depth=0):
"""Get the directory structure."""
if current_depth > max_depth:
return {}
structure = {}
try:
for item in os.listdir(project_path):
if should_ignore_file(item):
continue
item_path = os.path.join(project_path, item)
if os.path.isdir(item_path):
substructure = get_directory_structure(item_path, max_depth, current_depth + 1)
if substructure:
structure[item] = substructure
else:
structure[item] = None
except Exception as e:
print(f"Error scanning directory {project_path}: {e}")
return structure
def structure_to_tree(structure, prefix=''):
"""Convert directory structure to tree format."""
lines = []
items = sorted(list(structure.items()), key=lambda x: (x[1] is not None, x[0]))
for i, (name, substructure) in enumerate(items):
is_last = i == len(items) - 1
connector = '└─ ' if is_last else '├─ '
if substructure is None:
icon = '📄 '
lines.append(f"{prefix}{connector}{icon}{name}")
else:
icon = '📁 '
lines.append(f"{prefix}{connector}{icon}{name}")
extension = ' ' if is_last else '│ '
lines.extend(structure_to_tree(substructure, prefix + extension))
return lines
================================================
File: focus.py
================================================
import os
import time
from datetime import datetime
from .config import load_config, get_default_config
from .content_generator import generate_focus_content
from .rules_analyzer import RulesAnalyzer
from .rules_generator import RulesGenerator
def get_default_config():
"""Get default configuration with parent directory as project path."""
return {
'project_path': os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
'update_interval': 60,
'max_depth': 3,
'ignored_directories': [
'__pycache__',
'node_modules',
'venv',
'.git',
'.idea',
'.vscode',
'dist',
'build',
'CursorFocus'
],
'ignored_files': [
'.DS_Store',
'*.pyc',
'*.pyo'
],
'binary_extensions': [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.ico',
'.pdf',
'.exe',
'.bin'
],
'file_length_standards': {
'.js': 300,
'.jsx': 250,
'.ts': 300,
'.tsx': 250,
'.py': 400,
'.css': 400,
'.scss': 400,
'.less': 400,
'.sass': 400,
'.html': 300,
'.vue': 250,
'.svelte': 250,
'.json': 100,
'.yaml': 100,
'.yml': 100,
'.toml': 100,
'.md': 500,
'.rst': 500,
'default': 300
},
'file_length_thresholds': {
'warning': 1.0,
'critical': 1.5,
'severe': 2.0
},
'project_types': {
'chrome_extension': {
'indicators': ['manifest.json'],
'required_files': [],
'description': 'Chrome Extension'
},
'node_js': {
'indicators': ['package.json'],
'required_files': [],
'description': 'Node.js Project'
},
'python': {
'indicators': ['setup.py', 'pyproject.toml'],
'required_files': [],
'description': 'Python Project'
},
'react': {
'indicators': [],
'required_files': ['src/App.js', 'src/index.js'],
'description': 'React Application'
}
}
}
def setup_cursor_focus(project_path):
"""Set up CursorFocus for a project by generating necessary files."""
try:
# Generate .cursorrules file
print(f"Analyzing project: {project_path}")
analyzer = RulesAnalyzer(project_path)
project_info = analyzer.analyze_project_for_rules()
rules_generator = RulesGenerator(project_path)
rules_file = rules_generator.generate_rules_file(project_info)
print(f"✅ Generated {rules_file}")
# Generate initial Focus.md with default config
focus_file = os.path.join(project_path, 'Focus.md')
default_config = get_default_config()
content = generate_focus_content(project_path, default_config)
with open(focus_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ Generated {focus_file}")
print("\n🎉 CursorFocus setup complete!")
print("Generated files:")
print(f"- {rules_file}")
print(f"- {focus_file}")
except Exception as e:
print(f"❌ Error during setup: {e}")
raise
def monitor_project(project_config, global_config):
"""Monitor a single project."""
project_path = project_config['project_path']
print(f"\n🔍 Monitoring project: {project_config['name']} at {project_path}")
# Merge project config with global config
config = {**global_config, **project_config}
focus_file = os.path.join(project_path, 'Focus.md')
last_content = None
last_update = 0
while True:
current_time = time.time()
if current_time - last_update < config.get('update_interval', 60):
time.sleep(1)
continue
content = generate_focus_content(project_path, config)
if content != last_content:
try:
with open(focus_file, 'w', encoding='utf-8') as f:
f.write(content)
last_content = content
print(f"✅ {project_config['name']} Focus.md updated at {datetime.now().strftime('%I:%M:%S %p')}")
except Exception as e:
print(f"❌ Error writing Focus.md for {project_config['name']}: {e}")
last_update = current_time
def main():
"""Main function to monitor multiple projects."""
config = load_config()
if not config:
print("No config.json found, using default configuration")
config = get_default_config()
if 'projects' not in config:
# Handle single project config for backward compatibility
config['projects'] = [{
'name': 'Default Project',
'project_path': config['project_path'],
'update_interval': config.get('update_interval', 60),
'max_depth': config.get('max_depth', 3)
}]
# Create threads for each project
from threading import Thread
threads = []
try:
for project in config['projects']:
# Setup project if needed
rules_file = os.path.join(project['project_path'], '.cursorrules')
if not os.path.exists(rules_file):
setup_cursor_focus(project['project_path'])
# Start monitoring thread
thread = Thread(
target=monitor_project,
args=(project, config),
daemon=True
)
thread.start()
threads.append(thread)
print("\n📝 Press Ctrl+C to stop all monitors")
# Keep main thread alive
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n👋 Stopping all CursorFocus monitors")
except Exception as e:
print(f"\n❌ Error: {e}")
if __name__ == '__main__':
main()
================================================
File: install.sh
================================================
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}🚀 Installing CursorFocus...${NC}"
# Check if Python 3 is installed
if ! command -v python3 &> /dev/null; then
echo -e "${RED}❌ Python 3 is not installed. Please install Python 3 and try again.${NC}"
exit 1
fi
# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
echo -e "${BLUE}📦 Creating virtual environment...${NC}"
python3 -m venv venv
fi
# Activate virtual environment
echo -e "${BLUE}🔌 Activating virtual environment...${NC}"
source venv/bin/activate
# Install dependencies
echo -e "${BLUE}📚 Installing dependencies...${NC}"
pip install -e .
# Create .env file if it doesn't exist
if [ ! -f ".env" ]; then
echo -e "${BLUE}📝 Creating .env file...${NC}"
cp .env.example .env
echo -e "${GREEN}✅ Created .env file. Please edit it to add your Gemini API key.${NC}"
echo -e " Get your API key from: ${BLUE}https://makersuite.google.com/app/apikey${NC}"
fi
# Create config.json if it doesn't exist
if [ ! -f "config.json" ]; then
echo -e "${BLUE}⚙️ Creating config.json...${NC}"
cp config.example.json config.json
echo -e "${GREEN}✅ Created config.json. Please edit it to configure your project settings.${NC}"
fi
echo -e "${GREEN}✅ Installation complete!${NC}"
echo -e "\nNext steps:"
echo -e "1. Edit ${BLUE}.env${NC} to add your Gemini API key"
echo -e "2. Edit ${BLUE}config.json${NC} to configure your project settings"
echo -e "3. Run ${BLUE}cursorfocus${NC} for continuous monitoring"
echo -e " or ${BLUE}cursorfocus-review${NC} for a one-time code review"
================================================
File: project_detector.py
================================================
import os
import json
from config import load_config
# Load project types from config at module level
_config = load_config()
PROJECT_TYPES = _config.get('project_types', {})
def detect_project_type(project_path):
"""Detect project type based on file presence using configurable rules."""
for project_type, rules in PROJECT_TYPES.items():
# Check for indicator files
if any(os.path.exists(os.path.join(project_path, f)) for f in rules.get('indicators', [])):
return project_type
# Check for required files
if rules.get('required_files') and all(os.path.exists(os.path.join(project_path, f)) for f in rules['required_files']):
return project_type
return 'generic'
def scan_for_projects(root_path, max_depth=3, ignored_dirs=None):
"""Scan directory recursively for projects."""
if ignored_dirs is None:
ignored_dirs = _config.get('ignored_directories', [])
projects = []
root_path = os.path.abspath(root_path or '.')
# Kiểm tra thư mục gốc trước
project_type = detect_project_type(root_path)
if project_type != 'generic':
projects.append({
'path': root_path,
'type': project_type,
'name': os.path.basename(root_path)
})
def _scan_directory(current_path, current_depth):
if current_depth > max_depth:
return
try:
# Skip ignored directories
if any(ignored in current_path.split(os.path.sep) for ignored in ignored_dirs):
return
# Scan subdirectories
for item in os.listdir(current_path):
item_path = os.path.join(current_path, item)
if os.path.isdir(item_path):
# Kiểm tra từng thư mục con
project_type = detect_project_type(item_path)
if project_type != 'generic':
projects.append({
'path': item_path,
'type': project_type,
'name': item
})
else:
# Nếu không phải project thì quét tiếp
_scan_directory(item_path, current_depth + 1)
except (PermissionError, OSError):
# Skip directories we can't access
pass
# Bắt đầu quét từ thư mục gốc
_scan_directory(root_path, 0)
return projects
def get_project_description(project_path):
"""Get project description and key features using standardized approach."""
try:
project_type = detect_project_type(project_path)
project_info = {
"name": os.path.basename(project_path),
"description": "Project directory structure and information",
"key_features": [
f"Type: {PROJECT_TYPES.get(project_type, {'description': 'Generic Project'})['description']}",
"File and directory tracking",
"Automatic updates"
]
}
if project_type == 'chrome_extension':
manifest_path = os.path.join(project_path, 'manifest.json')
if os.path.exists(manifest_path):
with open(manifest_path, 'r') as f:
manifest_data = json.load(f)
project_info.update({
"name": manifest_data.get('name', 'Chrome Extension'),
"description": manifest_data.get('description', 'No description available'),
"key_features": [
f"Version: {manifest_data.get('version', 'unknown')}",
f"Type: {PROJECT_TYPES[project_type]['description']}",
*[f"Permission: {perm}" for perm in manifest_data.get('permissions', [])[:3]]
]
})
elif project_type == 'node_js':
package_path = os.path.join(project_path, 'package.json')
if os.path.exists(package_path):
with open(package_path, 'r') as f:
package_data = json.load(f)
project_info.update({
"name": package_data.get('name', 'Node.js Project'),
"description": package_data.get('description', 'No description available'),
"key_features": [
f"Version: {package_data.get('version', 'unknown')}",
f"Type: {PROJECT_TYPES[project_type]['description']}",
*[f"Dependency: {dep}" for dep in list(package_data.get('dependencies', {}).keys())[:3]]
]
})
return project_info
except Exception as e:
print(f"Error getting project description: {e}")
return {
"name": os.path.basename(project_path),
"description": "Error reading project information",
"key_features": ["File and directory tracking"]
}
def get_file_type_info(filename):
"""Get file type information."""
ext = os.path.splitext(filename)[1].lower()
type_map = {
'.py': ('Python Source', 'Python script containing project logic'),
'.js': ('JavaScript', 'JavaScript file for client-side functionality'),
'.jsx': ('React Component', 'React component file'),
'.ts': ('TypeScript', 'TypeScript source file'),
'.tsx': ('React TypeScript', 'React component with TypeScript'),
'.html': ('HTML', 'Web page template'),
'.css': ('CSS', 'Stylesheet for visual styling'),
'.md': ('Markdown', 'Documentation file'),
'.json': ('JSON', 'Configuration or data file')
}
return type_map.get(ext, ('Generic', 'Project file'))
================================================
File: requirements.txt
================================================
PyYAML>=6.0.1
watchdog>=3.0.0
python-dotenv>=1.0.0
colorama>=0.4.6
rich>=13.7.0
google-generativeai>=0.3.0
================================================
File: rules_analyzer.py
================================================
import os
import json
from typing import Dict, Any
class RulesAnalyzer:
def __init__(self, project_path: str):
self.project_path = project_path
def analyze_project_for_rules(self) -> Dict[str, Any]:
"""Analyze the project and return project information for rules generation."""
project_info = {
'name': self._detect_project_name(),
'version': '1.0.0',
'language': self._detect_main_language(),
'framework': self._detect_framework(),
'type': self._detect_project_type()
}
return project_info
def _detect_project_name(self) -> str:
"""Detect the project name from package files or directory name."""
# Try package.json
package_json_path = os.path.join(self.project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r') as f:
data = json.load(f)
if data.get('name'):
return data['name']
except:
pass
# Try setup.py
setup_py_path = os.path.join(self.project_path, 'setup.py')
if os.path.exists(setup_py_path):
try:
with open(setup_py_path, 'r') as f:
content = f.read()
if 'name=' in content:
# Simple extraction, could be improved
name = content.split('name=')[1].split(',')[0].strip("'\"")
if name:
return name
except:
pass
# Default to directory name
return os.path.basename(os.path.abspath(self.project_path))
def _detect_main_language(self) -> str:
"""Detect the main programming language used in the project."""
extensions = {}
for root, _, files in os.walk(self.project_path):
if 'node_modules' in root or 'venv' in root or '.git' in root:
continue
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext:
extensions[ext] = extensions.get(ext, 0) + 1
# Map extensions to languages
language_map = {
'.py': 'python',
'.js': 'javascript',
'.ts': 'typescript',
'.jsx': 'javascript',
'.tsx': 'typescript',
'.java': 'java',
'.rb': 'ruby',
'.php': 'php',
'.go': 'go'
}
# Find the most common language
max_count = 0
main_language = 'javascript' # default
for ext, count in extensions.items():
if ext in language_map and count > max_count:
max_count = count
main_language = language_map[ext]
return main_language
def _detect_framework(self) -> str:
"""Detect the framework used in the project."""
# Check package.json for JS/TS frameworks
package_json_path = os.path.join(self.project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r') as f:
data = json.load(f)
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
if 'react' in deps:
return 'react'
if 'vue' in deps:
return 'vue'
if '@angular/core' in deps:
return 'angular'
if 'next' in deps:
return 'next.js'
if 'express' in deps:
return 'express'
except:
pass
# Check requirements.txt for Python frameworks
requirements_path = os.path.join(self.project_path, 'requirements.txt')
if os.path.exists(requirements_path):
try:
with open(requirements_path, 'r') as f:
content = f.read().lower()
if 'django' in content:
return 'django'
if 'flask' in content:
return 'flask'
if 'fastapi' in content:
return 'fastapi'
except:
pass
return 'none'
def _detect_project_type(self) -> str:
"""Detect the type of project (web, mobile, library, etc.)."""
package_json_path = os.path.join(self.project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r') as f:
data = json.load(f)
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
# Check for mobile frameworks
if 'react-native' in deps or '@ionic/core' in deps:
return 'mobile application'
# Check for desktop frameworks
if 'electron' in deps:
return 'desktop application'
# Check if it's a library
if data.get('name', '').startswith('@') or '-lib' in data.get('name', ''):
return 'library'
except:
pass
# Look for common web project indicators
web_indicators = ['index.html', 'public/index.html', 'src/index.html']
for indicator in web_indicators:
if os.path.exists(os.path.join(self.project_path, indicator)):
return 'web application'
return 'application'
================================================
File: rules_generator.py
================================================
import os
import json
from typing import Dict, Any, List
from datetime import datetime
class RulesGenerator:
def __init__(self, project_path: str):
self.project_path = project_path
self.template_path = os.path.join(os.path.dirname(__file__), 'templates', 'default.cursorrules.json')
self.focus_template_path = os.path.join(os.path.dirname(__file__), 'templates', 'Focus.md')
def _get_timestamp(self) -> str:
"""Get current timestamp in standard format."""
return datetime.now().strftime('%B %d, %Y at %I:%M %p')
def generate_rules_file(self, project_info: Dict[str, Any]) -> str:
"""Generate the .cursorrules file based on project analysis."""
# Load template
template = self._load_template()
# Customize template
rules = self._customize_template(template, project_info)
# Write to file
rules_file = os.path.join(self.project_path, '.cursorrules')
with open(rules_file, 'w', encoding='utf-8') as f:
json.dump(rules, f, indent=2)
return rules_file
def _load_template(self) -> Dict[str, Any]:
"""Load the default template."""
try:
with open(self.template_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading template: {e}")
return self._get_default_template()
def _customize_template(self, template: Dict[str, Any], project_info: Dict[str, Any]) -> Dict[str, Any]:
"""Customize the template based on project analysis."""
rules = template.copy()
# Add timestamp first
rules['last_updated'] = self._get_timestamp()
# Update project info
rules['project'].update(project_info)
# Add framework-specific rules
if project_info['framework'] != 'none':
framework_rules = self._get_framework_rules(project_info['framework'])
rules['ai_behavior']['code_generation']['style']['prefer'].extend(framework_rules)
# Add language-specific rules
language_rules = self._get_language_rules(project_info['language'])
rules['ai_behavior']['code_generation']['style']['prefer'].extend(language_rules)
# Add project-type specific rules
self._add_project_type_rules(rules, project_info['type'])
# Update testing frameworks
rules['ai_behavior']['testing']['frameworks'] = self._detect_testing_frameworks()
return rules
def _get_framework_rules(self, framework: str) -> List[str]:
"""Get framework-specific coding rules."""
framework_rules = {
'react': [
'use functional components over class components',
'prefer hooks for state management',
'use memo for performance optimization'
],
'vue': [
'use composition API',
'prefer ref/reactive for state management',
'use script setup syntax'
],
'angular': [
'follow angular style guide',
'use observables for async operations',
'implement lifecycle hooks properly'
],
'django': [
'follow Django best practices',
'use class-based views when appropriate',
'implement proper model relationships'
],
'flask': [
'use Flask blueprints for organization',
'implement proper error handling',
'use Flask-SQLAlchemy for database operations'
]
}
return framework_rules.get(framework.lower(), [])
def _get_language_rules(self, language: str) -> List[str]:
"""Get language-specific coding rules."""
language_rules = {
'python': [
'follow PEP 8 guidelines',
'use type hints',
'prefer list comprehension when appropriate'
],
'javascript': [
'use modern ES features',
'prefer arrow functions',
'use optional chaining'
],
'typescript': [
'use strict type checking',
'leverage type inference',
'use interface over type when possible'
]
}
return language_rules.get(language.lower(), [])
def _detect_testing_frameworks(self) -> List[str]:
"""Detect testing frameworks used in the project."""
testing_frameworks = []
# Check package.json for JS/TS testing frameworks
package_json_path = os.path.join(self.project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r') as f:
data = json.load(f)
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
if 'jest' in deps:
testing_frameworks.append('jest')
if 'mocha' in deps:
testing_frameworks.append('mocha')
if '@testing-library/react' in deps:
testing_frameworks.append('testing-library')
except:
pass
# Check requirements.txt for Python testing frameworks
requirements_path = os.path.join(self.project_path, 'requirements.txt')
if os.path.exists(requirements_path):
try:
with open(requirements_path, 'r') as f:
content = f.read().lower()
if 'pytest' in content:
testing_frameworks.append('pytest')
if 'unittest' in content:
testing_frameworks.append('unittest')
except:
pass
return testing_frameworks if testing_frameworks else ['jest'] # Default to jest
def _add_project_type_rules(self, rules: Dict[str, Any], project_type: str):
"""Add project-type specific rules."""
type_rules = {
'web application': {
'accessibility': {'required': True},
'performance': {
'prefer': [
'code splitting',
'lazy loading',
'performance monitoring'
]
}
},
'mobile application': {
'performance': {
'prefer': [
'offline first',
'battery optimization',
'responsive design'
]
}
},
'library': {
'documentation': {'required': True},
'testing': {'coverage_threshold': 90}
}
}
specific_rules = type_rules.get(project_type.lower())
if specific_rules:
rules['ai_behavior'].update(specific_rules)
def _get_default_template(self) -> Dict[str, Any]:
"""Get a default template if the template file cannot be loaded."""
return {
"version": "1.0",
"last_updated": self._get_timestamp(),
"project": {
"name": "Unknown Project",
"version": "1.0.0",
"language": "javascript",
"framework": "none",
"type": "application"
},
"ai_behavior": {
"code_generation": {
"style": {
"prefer": [],
"avoid": [
"magic numbers",
"nested callbacks",
"hard-coded values"
]
}
},
"testing": {
"required": True,
"frameworks": ["jest"],
"coverage_threshold": 80
}
}
}
================================================
File: run.sh
================================================
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Help message
show_help() {
echo "Usage: ./run.sh [options]"
echo ""
echo "Options:"
echo " --scan [path] Scan directory for projects (default: current directory)"
echo " --help Show this help message"
echo ""
echo "Examples:"
echo " ./run.sh # Run with default configuration"
echo " ./run.sh --scan # Scan current directory for projects"
echo " ./run.sh --scan ~/projects # Scan specific directory for projects"
}
# Parse command line arguments
SCAN_MODE=false
SCAN_PATH="."
while [[ $# -gt 0 ]]; do
case $1 in
--scan)
SCAN_MODE=true
if [ ! -z "$2" ] && [ ${2:0:1} != "-" ]; then
SCAN_PATH="$2"
shift
fi
shift
;;
--help)
show_help
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
show_help
exit 1
;;
esac
done
echo -e "${BLUE}🚀 Starting CursorFocus...${NC}"
# Check if Python 3 is installed
if ! command -v python3 &> /dev/null; then
echo -e "${RED}❌ Python 3 is not installed. Please install Python 3 and try again.${NC}"
exit 1
fi
# Check if required Python packages are installed
echo -e "${BLUE}📦 Checking dependencies...${NC}"
pip3 install -r "$SCRIPT_DIR/requirements.txt" > /dev/null 2>&1
if [ "$SCAN_MODE" = true ]; then
echo -e "${BLUE}🔍 Scanning for projects in: $SCAN_PATH${NC}"
python3 "$SCRIPT_DIR/setup.py" --scan "$SCAN_PATH"
exit $?
fi
# Get the parent directory (project root)
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Check if config.json exists, if not create it from example
if [ ! -f "$SCRIPT_DIR/config.json" ]; then
echo -e "${BLUE}📝 Creating configuration from template...${NC}"
if [ -f "$SCRIPT_DIR/config.example.json" ]; then
# Create config.json from example and replace placeholder path
sed "s|/path/to/your/project|$PROJECT_ROOT|g" "$SCRIPT_DIR/config.example.json" > "$SCRIPT_DIR/config.json"
echo -e "${GREEN}✅ Configuration created from template${NC}"
else
echo -e "${RED}❌ config.example.json not found. Please check the installation.${NC}"
exit 1
fi
fi
# Run CursorFocus
echo -e "${BLUE}🔍 Starting CursorFocus monitor...${NC}"
cd "$PROJECT_ROOT"
python3 "$SCRIPT_DIR/focus.py"
================================================
File: setup.py
================================================
from setuptools import setup, find_packages
setup(
name="cursorfocus",
version="1.0.0",
packages=find_packages(),
install_requires=[
"PyYAML>=6.0.1",
"watchdog>=3.0.0",
"python-dotenv>=1.0.0",
"colorama>=0.4.6",
"rich>=13.7.0",
"google-generativeai>=0.3.0"
],
entry_points={
"console_scripts": [
"cursorfocus=cursorfocus.focus:main",
"cursorfocus-review=cursorfocus.code_review:main"
]
},
author="Dror Bengal",
author_email="[email protected]",
description="AI-powered code review and project analysis tool",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/Dror-Bengal/CursorFocus",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Software Development :: Documentation",
"Topic :: Software Development :: Quality Assurance",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
],
python_requires=">=3.8",
include_package_data=True,
package_data={
"cursorfocus": ["templates/*"],
}
)
================================================
File: .env.example
================================================
# Get your API key from: https://makersuite.google.com/app/apikey
GEMINI_API_KEY=your_api_key_here
# Optional configuration
UPDATE_INTERVAL=60 # Update interval in seconds for monitoring
PROJECT_PATH=. # Default to current directory
================================================
File: templates/default.cursorrules.json
================================================
{
"version": "1.0",
"last_updated": "",
"project": {
"name": "Your Project",
"version": "1.0.0",
"language": "javascript",
"framework": "none",
"type": "application"
},
"ai_behavior": {
"code_generation": {
"style": {
"prefer": [
"async/await over callbacks",
"const over let",
"descriptive variable names",
"single responsibility functions"
],
"avoid": [
"magic numbers",
"nested callbacks",
"hard-coded values",
"complex conditionals"
]
},
"error_handling": {
"prefer": [
"try/catch for async operations",
"custom error messages",
"meaningful error states"
],
"avoid": [
"silent errors",
"empty catch blocks",
"generic error messages"
]
},
"performance": {
"prefer": [
"lazy loading",
"debouncing and throttling for events",
"memoization for expensive calculations"
],
"avoid": [
"blocking synchronous code",
"large inline scripts",
"unnecessary re-renders"
]
}
},
"testing": {
"required": true,
"frameworks": ["jest"],
"coverage_threshold": 80,
"include": [
"unit tests for new functions",
"integration tests for critical workflows",
"edge case scenarios"
]
},
"security": {
"sensitive_patterns": [
"API_KEY",
"SECRET",
"PASSWORD",
"CREDENTIAL"
],
"protected_files": [
"config/*.json",
".env*"
],
"sanitize_input": true,
"validate_user_data": true,
"avoid_eval": true
},
"accessibility": {
"standards": ["WCAG 2.1"],
"require_alt_text": true,
"focus_indicators": true,
"aria_labels": true
}
},
"communication": {
"style": "step-by-step",
"level": "beginner-friendly",
"on_error": [
"log error details",
"suggest alternative solutions",
"ask for clarification if unsure"
],
"on_success": [
"summarize changes",
"provide context for future improvements",
"highlight any potential optimizations"
],
"confirmations": {
"required_for": [
"major changes",
"file deletions",
"dependency updates",
"structural changes"
]
}
},
"response_format": {
"always": [
"show file paths",
"explain changes simply",
"highlight modified sections only",
"provide next steps"
],
"never": [
"create new files without permission",
"remove existing code without confirmation",
"use technical jargon without explanation",
"show entire files unless requested"
]
}
}
================================================
File: ~/Library/LaunchAgents/com.cursorfocus.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cursorfocus</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/python3</string>
<string>/Users/drorbengal/test1212/CursorFocus/focus.py</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/drorbengal/test1212/CursorFocus</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/drorbengal/test1212/CursorFocus/cursorfocus.log</string>
<key>StandardErrorPath</key>
<string>/Users/drorbengal/test1212/CursorFocus/cursorfocus.error.log</string>
</dict>
</plist>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment