cicada-mcp 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cicada/_version_hash.py +4 -0
- cicada/cli.py +6 -748
- cicada/commands.py +1255 -0
- cicada/dead_code/__init__.py +1 -0
- cicada/{find_dead_code.py → dead_code/finder.py} +2 -1
- cicada/dependency_analyzer.py +147 -0
- cicada/entry_utils.py +92 -0
- cicada/extractors/base.py +9 -9
- cicada/extractors/call.py +17 -20
- cicada/extractors/common.py +64 -0
- cicada/extractors/dependency.py +117 -235
- cicada/extractors/doc.py +2 -49
- cicada/extractors/function.py +10 -14
- cicada/extractors/keybert.py +228 -0
- cicada/extractors/keyword.py +191 -0
- cicada/extractors/module.py +6 -10
- cicada/extractors/spec.py +8 -56
- cicada/format/__init__.py +20 -0
- cicada/{ascii_art.py → format/ascii_art.py} +1 -1
- cicada/format/formatter.py +1145 -0
- cicada/git_helper.py +134 -7
- cicada/indexer.py +322 -89
- cicada/interactive_setup.py +251 -323
- cicada/interactive_setup_helpers.py +302 -0
- cicada/keyword_expander.py +437 -0
- cicada/keyword_search.py +208 -422
- cicada/keyword_test.py +383 -16
- cicada/mcp/__init__.py +10 -0
- cicada/mcp/entry.py +17 -0
- cicada/mcp/filter_utils.py +107 -0
- cicada/mcp/pattern_utils.py +118 -0
- cicada/{mcp_server.py → mcp/server.py} +819 -73
- cicada/mcp/tools.py +473 -0
- cicada/pr_finder.py +2 -3
- cicada/pr_indexer/indexer.py +3 -2
- cicada/setup.py +167 -35
- cicada/tier.py +225 -0
- cicada/utils/__init__.py +9 -2
- cicada/utils/fuzzy_match.py +54 -0
- cicada/utils/index_utils.py +9 -0
- cicada/utils/path_utils.py +18 -0
- cicada/utils/text_utils.py +52 -1
- cicada/utils/tree_utils.py +47 -0
- cicada/version_check.py +99 -0
- cicada/watch_manager.py +320 -0
- cicada/watcher.py +431 -0
- cicada_mcp-0.3.0.dist-info/METADATA +541 -0
- cicada_mcp-0.3.0.dist-info/RECORD +70 -0
- cicada_mcp-0.3.0.dist-info/entry_points.txt +4 -0
- cicada/formatter.py +0 -864
- cicada/keybert_extractor.py +0 -286
- cicada/lightweight_keyword_extractor.py +0 -290
- cicada/mcp_entry.py +0 -683
- cicada/mcp_tools.py +0 -291
- cicada_mcp-0.2.0.dist-info/METADATA +0 -735
- cicada_mcp-0.2.0.dist-info/RECORD +0 -53
- cicada_mcp-0.2.0.dist-info/entry_points.txt +0 -4
- /cicada/{dead_code_analyzer.py → dead_code/analyzer.py} +0 -0
- /cicada/{colors.py → format/colors.py} +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/top_level.txt +0 -0
cicada/git_helper.py
CHANGED
|
@@ -11,6 +11,7 @@ Author: Cursor(Auto)
|
|
|
11
11
|
import subprocess
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
14
15
|
|
|
15
16
|
import git
|
|
16
17
|
|
|
@@ -368,6 +369,11 @@ class GitHelper:
|
|
|
368
369
|
# Parse porcelain format
|
|
369
370
|
lines_data = []
|
|
370
371
|
current_commit = {}
|
|
372
|
+
# Cache commit metadata by SHA to handle repeated commits
|
|
373
|
+
# Optimization: Pre-validate commits during caching to avoid validation in hot loop
|
|
374
|
+
commit_cache = {}
|
|
375
|
+
# Track which commits have all required fields (valid)
|
|
376
|
+
valid_commits = set()
|
|
371
377
|
|
|
372
378
|
for line in result.stdout.split("\n"):
|
|
373
379
|
if not line:
|
|
@@ -377,11 +383,21 @@ class GitHelper:
|
|
|
377
383
|
if len(line) >= 40 and line[0:40].isalnum():
|
|
378
384
|
parts = line.split()
|
|
379
385
|
if len(parts) >= 3:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
sha = parts[0]
|
|
387
|
+
# Check if we've seen this commit before
|
|
388
|
+
if sha in commit_cache:
|
|
389
|
+
# Reuse cached metadata
|
|
390
|
+
current_commit = {
|
|
391
|
+
**commit_cache[sha],
|
|
392
|
+
"line_number": int(parts[2]),
|
|
393
|
+
}
|
|
394
|
+
else:
|
|
395
|
+
# New commit - initialize with SHA and line number
|
|
396
|
+
current_commit = {
|
|
397
|
+
"sha": sha[:8],
|
|
398
|
+
"full_sha": sha,
|
|
399
|
+
"line_number": int(parts[2]),
|
|
400
|
+
}
|
|
385
401
|
# Author name
|
|
386
402
|
elif line.startswith("author "):
|
|
387
403
|
current_commit["author"] = line[7:]
|
|
@@ -396,11 +412,24 @@ class GitHelper:
|
|
|
396
412
|
current_commit["date"] = datetime.fromtimestamp(timestamp).isoformat()
|
|
397
413
|
except (ValueError, OSError):
|
|
398
414
|
current_commit["date"] = line[12:]
|
|
415
|
+
# Cache this commit's metadata and validate (after we have all fields)
|
|
416
|
+
if "author" in current_commit and "author_email" in current_commit:
|
|
417
|
+
commit_cache[current_commit["full_sha"]] = {
|
|
418
|
+
"sha": current_commit["sha"],
|
|
419
|
+
"full_sha": current_commit["full_sha"],
|
|
420
|
+
"author": current_commit["author"],
|
|
421
|
+
"author_email": current_commit["author_email"],
|
|
422
|
+
"date": current_commit["date"],
|
|
423
|
+
}
|
|
424
|
+
# Mark as valid to avoid per-line validation in hot loop
|
|
425
|
+
valid_commits.add(current_commit["full_sha"])
|
|
399
426
|
# Actual code line (starts with tab)
|
|
400
427
|
elif line.startswith("\t"):
|
|
401
428
|
code_line = line[1:] # Remove leading tab
|
|
402
|
-
|
|
403
|
-
|
|
429
|
+
# Use pre-validated commit check (optimized - no field iteration per line)
|
|
430
|
+
if current_commit.get("full_sha") in valid_commits:
|
|
431
|
+
line_info = {**current_commit, "content": code_line}
|
|
432
|
+
lines_data.append(line_info)
|
|
404
433
|
|
|
405
434
|
# Group consecutive lines by same author and commit
|
|
406
435
|
if lines_data:
|
|
@@ -580,6 +609,104 @@ class GitHelper:
|
|
|
580
609
|
|
|
581
610
|
return results
|
|
582
611
|
|
|
612
|
+
def get_file_history_filtered(
|
|
613
|
+
self,
|
|
614
|
+
file_path: str,
|
|
615
|
+
max_commits: int = 10,
|
|
616
|
+
since_date: datetime | None = None,
|
|
617
|
+
until_date: datetime | None = None,
|
|
618
|
+
author: str | None = None,
|
|
619
|
+
min_changes: int = 0,
|
|
620
|
+
) -> list[dict]:
|
|
621
|
+
"""
|
|
622
|
+
Get commit history for a file with advanced filtering options.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
file_path: Relative path to file from repo root
|
|
626
|
+
max_commits: Maximum number of commits to return
|
|
627
|
+
since_date: Only include commits after this date
|
|
628
|
+
until_date: Only include commits before this date
|
|
629
|
+
author: Filter by author name (substring match, case-insensitive)
|
|
630
|
+
min_changes: Minimum number of lines changed (insertions + deletions)
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
List of commit information dictionaries with keys:
|
|
634
|
+
- sha: Short commit SHA (8 chars)
|
|
635
|
+
- full_sha: Full commit SHA
|
|
636
|
+
- author: Author name
|
|
637
|
+
- author_email: Author email
|
|
638
|
+
- date: Commit date in ISO format
|
|
639
|
+
- message: Full commit message
|
|
640
|
+
- summary: First line of commit message
|
|
641
|
+
- insertions: Number of lines inserted (if min_changes > 0)
|
|
642
|
+
- deletions: Number of lines deleted (if min_changes > 0)
|
|
643
|
+
"""
|
|
644
|
+
commits = []
|
|
645
|
+
author_lower = author.lower() if author else None
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
# Get commits that touched this file
|
|
649
|
+
for commit in self.repo.iter_commits(paths=file_path):
|
|
650
|
+
# Apply date filters
|
|
651
|
+
commit_date = commit.committed_datetime.replace(tzinfo=None)
|
|
652
|
+
if since_date and commit_date < since_date:
|
|
653
|
+
continue
|
|
654
|
+
if until_date and commit_date > until_date:
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
# Apply author filter
|
|
658
|
+
if author_lower and author_lower not in str(commit.author).lower():
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# Apply min_changes filter if specified
|
|
662
|
+
if min_changes > 0:
|
|
663
|
+
try:
|
|
664
|
+
# Get stats for this specific file in this commit
|
|
665
|
+
file_stats = commit.stats.files.get(file_path, {})
|
|
666
|
+
insertions = int(file_stats.get("insertions", 0)) if file_stats else 0 # type: ignore
|
|
667
|
+
deletions = int(file_stats.get("deletions", 0)) if file_stats else 0 # type: ignore
|
|
668
|
+
total_changes = insertions + deletions
|
|
669
|
+
|
|
670
|
+
if total_changes < min_changes:
|
|
671
|
+
continue
|
|
672
|
+
except Exception:
|
|
673
|
+
# If we can't get stats, skip the filter
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
# Build commit info
|
|
677
|
+
commit_info: dict[str, Any] = {
|
|
678
|
+
"sha": commit.hexsha[:8],
|
|
679
|
+
"full_sha": commit.hexsha,
|
|
680
|
+
"author": str(commit.author),
|
|
681
|
+
"author_email": commit.author.email,
|
|
682
|
+
"date": commit.committed_datetime.isoformat(),
|
|
683
|
+
"message": commit.message.strip(),
|
|
684
|
+
"summary": commit.summary,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
# Add change stats if min_changes was specified
|
|
688
|
+
if min_changes > 0:
|
|
689
|
+
try:
|
|
690
|
+
file_stats = commit.stats.files.get(file_path, {})
|
|
691
|
+
# Type: ignore - file_stats is dict with string keys, values can be int
|
|
692
|
+
insertions = int(file_stats.get("insertions", 0)) if file_stats else 0 # type: ignore
|
|
693
|
+
deletions = int(file_stats.get("deletions", 0)) if file_stats else 0 # type: ignore
|
|
694
|
+
commit_info["insertions"] = insertions
|
|
695
|
+
commit_info["deletions"] = deletions
|
|
696
|
+
except Exception:
|
|
697
|
+
commit_info["insertions"] = 0
|
|
698
|
+
commit_info["deletions"] = 0
|
|
699
|
+
|
|
700
|
+
commits.append(commit_info)
|
|
701
|
+
|
|
702
|
+
if len(commits) >= max_commits:
|
|
703
|
+
break
|
|
704
|
+
|
|
705
|
+
except Exception as e:
|
|
706
|
+
print(f"Error getting filtered history for {file_path}: {e}")
|
|
707
|
+
|
|
708
|
+
return commits
|
|
709
|
+
|
|
583
710
|
|
|
584
711
|
def main():
|
|
585
712
|
"""Test git helper functions"""
|