Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions codemcp/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,23 @@ def truncate_output_content(content: str, prefer_end: bool = True) -> str:

def find_git_root(start_path: str) -> Optional[str]:
"""Find the root of the Git repository starting from the given path.

Args:
start_path: The path to start searching from

Returns:
The absolute path to the Git repository root, or None if not found
"""
path = os.path.abspath(start_path)

while path:
if os.path.isdir(os.path.join(path, ".git")):
return path

parent = os.path.dirname(path)
if parent == path: # Reached filesystem root
return None

path = parent

return None
36 changes: 18 additions & 18 deletions codemcp/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def load_rule_from_file(file_path: str) -> Optional[Rule]:

# Extract rule properties
description = frontmatter.get("description")

# Handle globs - can be comma-separated string or a list
globs_value = frontmatter.get("globs")
globs: Optional[List[str]] = None
Expand All @@ -65,7 +65,7 @@ def load_rule_from_file(file_path: str) -> Optional[Rule]:
globs = [g.strip() for g in globs_value.split(",")]
elif isinstance(globs_value, list):
globs = globs_value

always_apply = frontmatter.get("alwaysApply", False)

return Rule(
Expand All @@ -82,7 +82,7 @@ def load_rule_from_file(file_path: str) -> Optional[Rule]:

def match_file_with_glob(file_path: str, glob_pattern: str) -> bool:
"""Check if a file path matches a glob pattern.

Args:
file_path: Path to check
glob_pattern: Glob pattern to match against
Expand All @@ -92,22 +92,22 @@ def match_file_with_glob(file_path: str, glob_pattern: str) -> bool:
"""
# Convert to Path object for consistent handling
path = Path(file_path)

# Handle ** pattern (recursive wildcard)
if "**" in glob_pattern:
# Split the pattern into parts for matching
parts = glob_pattern.split("**")
if len(parts) != 2:
# We only support simple patterns with one ** for now
return False

prefix, suffix = parts

# Check if the file path starts with the prefix and ends with the suffix
return (prefix == "" or str(path).startswith(prefix)) and (
suffix == "" or str(path).endswith(suffix)
)

# Use fnmatch for simple glob patterns
return fnmatch.fnmatch(str(path), glob_pattern)

Expand All @@ -116,14 +116,14 @@ def find_applicable_rules(
repo_root: str, file_path: Optional[str] = None
) -> Tuple[List[Rule], List[Tuple[str, str]]]:
"""Find all applicable rules for the given file path.

Walks up the directory tree from the file path to the repo root,
looking for .cursor/rules directories and loading MDC files.

Args:
repo_root: Root of the repository
file_path: Optional path to a file to match against rules

Returns:
A tuple containing (applicable_rules, suggested_rules)
- applicable_rules: List of Rule objects that match the file
Expand All @@ -132,15 +132,15 @@ def find_applicable_rules(
applicable_rules: List[Rule] = []
suggested_rules: List[Tuple[str, str]] = []
processed_rule_files: Set[str] = set()

# Normalize paths
repo_root = os.path.abspath(repo_root)

# If file_path is provided, walk up from its directory to repo_root
# Otherwise, just check repo_root
start_dir = os.path.dirname(os.path.abspath(file_path)) if file_path else repo_root
current_dir = start_dir

# Ensure we don't go beyond repo_root
while current_dir.startswith(repo_root):
# Look for .cursor/rules directory
Expand All @@ -151,17 +151,17 @@ def find_applicable_rules(
for filename in files:
if filename.endswith(".mdc"):
rule_file_path = os.path.join(root, filename)

# Skip if we've already processed this file
if rule_file_path in processed_rule_files:
continue
processed_rule_files.add(rule_file_path)

# Load the rule
rule = load_rule_from_file(rule_file_path)
if rule is None:
continue

# Check if this rule applies
if rule.always_apply:
applicable_rules.append(rule)
Expand All @@ -174,11 +174,11 @@ def find_applicable_rules(
elif rule.description:
# Add to suggested rules if it has a description
suggested_rules.append((rule.description, rule_file_path))

# Move up one directory
parent_dir = os.path.dirname(current_dir)
if parent_dir == current_dir: # We've reached the root
break
current_dir = parent_dir

return applicable_rules, suggested_rules
12 changes: 7 additions & 5 deletions codemcp/tools/read_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,22 @@ async def read_file_content(
try:
# Find git repository root
repo_root = find_git_root(os.path.dirname(full_file_path))

if repo_root:
# Find applicable rules
applicable_rules, suggested_rules = find_applicable_rules(repo_root, full_file_path)

applicable_rules, suggested_rules = find_applicable_rules(
repo_root, full_file_path
)

# If we have applicable rules, add them to the output
if applicable_rules or suggested_rules:
content += "\n\n// .cursor/rules results:"

# Add directly applicable rules
for rule in applicable_rules:
rule_content = f"\n\n// Rule from {os.path.relpath(rule.file_path, repo_root)}:\n{rule.payload}"
content += rule_content

# Add suggestions for rules with descriptions
for description, rule_path in suggested_rules:
rel_path = os.path.relpath(rule_path, repo_root)
Expand Down
14 changes: 7 additions & 7 deletions codemcp/tools/user_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,32 @@ async def user_prompt(user_text: str, chat_id: str | None = None) -> str:
"""
try:
logging.info(f"Received user prompt for chat ID {chat_id}: {user_text}")

# Get the current working directory to find repo root
cwd = os.getcwd()
repo_root = find_git_root(cwd)

result = "User prompt received"

# If we're in a git repo, look for applicable rules
if repo_root:
# Find applicable rules (no file path for UserPrompt)
applicable_rules, suggested_rules = find_applicable_rules(repo_root)

# If we have applicable rules, add them to the result
if applicable_rules or suggested_rules:
result += "\n\n// .cursor/rules results:"

# Add directly applicable rules (alwaysApply=true)
for rule in applicable_rules:
rule_content = f"\n\n// Rule from {os.path.relpath(rule.file_path, repo_root)}:\n{rule.payload}"
result += rule_content

# Add suggestions for rules with descriptions
for description, rule_path in suggested_rules:
rel_path = os.path.relpath(rule_path, repo_root)
result += f"\n\n// If {description} applies, load {rel_path}"

return result
except Exception as e:
logging.warning(f"Exception suppressed in user_prompt: {e!s}", exc_info=True)
Expand Down
8 changes: 4 additions & 4 deletions e2e/test_cursor_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ async def test_read_file_with_rules(self):
# Verify that the JavaScript rule was applied
self.assertIn("// .cursor/rules results:", result)
self.assertIn("Use camelCase for variable names in JavaScript", result)

# Verify that the always-apply rule was applied
self.assertIn("Follow PEP 8 guidelines", result)

# Verify that the suggested rule appears
self.assertIn("If For code that needs optimization applies", result)

Expand All @@ -113,7 +113,7 @@ async def test_read_file_with_rules(self):
# Verify that the Python rule was applied
self.assertIn("// .cursor/rules results:", result)
self.assertIn("Use snake_case for variable names in Python", result)

# Verify that the always-apply rule was applied
self.assertIn("Follow PEP 8 guidelines", result)

Expand All @@ -124,7 +124,7 @@ async def test_user_prompt_with_rules(self):
# Verify that the always-apply rule was applied
self.assertIn("// .cursor/rules results:", result)
self.assertIn("Follow PEP 8 guidelines", result)

# Verify that the suggested rule appears
self.assertIn("If For code that needs optimization applies", result)

Expand Down
6 changes: 4 additions & 2 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ def test_match_file_with_glob(self):
self.assertTrue(match_file_with_glob("test.js", "*.js"))
self.assertTrue(match_file_with_glob("/path/to/test.js", "*.js"))
self.assertTrue(match_file_with_glob("/path/to/test.js", "**/*.js"))
self.assertTrue(match_file_with_glob("/path/to/src/components/Button.jsx", "src/**/*.jsx"))

self.assertTrue(
match_file_with_glob("/path/to/src/components/Button.jsx", "src/**/*.jsx")
)

# Test non-matching paths
self.assertFalse(match_file_with_glob("test.py", "*.js"))
self.assertFalse(match_file_with_glob("/path/to/test.ts", "*.js"))
Expand Down
Loading