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.
Files changed (62) hide show
  1. cicada/_version_hash.py +4 -0
  2. cicada/cli.py +6 -748
  3. cicada/commands.py +1255 -0
  4. cicada/dead_code/__init__.py +1 -0
  5. cicada/{find_dead_code.py → dead_code/finder.py} +2 -1
  6. cicada/dependency_analyzer.py +147 -0
  7. cicada/entry_utils.py +92 -0
  8. cicada/extractors/base.py +9 -9
  9. cicada/extractors/call.py +17 -20
  10. cicada/extractors/common.py +64 -0
  11. cicada/extractors/dependency.py +117 -235
  12. cicada/extractors/doc.py +2 -49
  13. cicada/extractors/function.py +10 -14
  14. cicada/extractors/keybert.py +228 -0
  15. cicada/extractors/keyword.py +191 -0
  16. cicada/extractors/module.py +6 -10
  17. cicada/extractors/spec.py +8 -56
  18. cicada/format/__init__.py +20 -0
  19. cicada/{ascii_art.py → format/ascii_art.py} +1 -1
  20. cicada/format/formatter.py +1145 -0
  21. cicada/git_helper.py +134 -7
  22. cicada/indexer.py +322 -89
  23. cicada/interactive_setup.py +251 -323
  24. cicada/interactive_setup_helpers.py +302 -0
  25. cicada/keyword_expander.py +437 -0
  26. cicada/keyword_search.py +208 -422
  27. cicada/keyword_test.py +383 -16
  28. cicada/mcp/__init__.py +10 -0
  29. cicada/mcp/entry.py +17 -0
  30. cicada/mcp/filter_utils.py +107 -0
  31. cicada/mcp/pattern_utils.py +118 -0
  32. cicada/{mcp_server.py → mcp/server.py} +819 -73
  33. cicada/mcp/tools.py +473 -0
  34. cicada/pr_finder.py +2 -3
  35. cicada/pr_indexer/indexer.py +3 -2
  36. cicada/setup.py +167 -35
  37. cicada/tier.py +225 -0
  38. cicada/utils/__init__.py +9 -2
  39. cicada/utils/fuzzy_match.py +54 -0
  40. cicada/utils/index_utils.py +9 -0
  41. cicada/utils/path_utils.py +18 -0
  42. cicada/utils/text_utils.py +52 -1
  43. cicada/utils/tree_utils.py +47 -0
  44. cicada/version_check.py +99 -0
  45. cicada/watch_manager.py +320 -0
  46. cicada/watcher.py +431 -0
  47. cicada_mcp-0.3.0.dist-info/METADATA +541 -0
  48. cicada_mcp-0.3.0.dist-info/RECORD +70 -0
  49. cicada_mcp-0.3.0.dist-info/entry_points.txt +4 -0
  50. cicada/formatter.py +0 -864
  51. cicada/keybert_extractor.py +0 -286
  52. cicada/lightweight_keyword_extractor.py +0 -290
  53. cicada/mcp_entry.py +0 -683
  54. cicada/mcp_tools.py +0 -291
  55. cicada_mcp-0.2.0.dist-info/METADATA +0 -735
  56. cicada_mcp-0.2.0.dist-info/RECORD +0 -53
  57. cicada_mcp-0.2.0.dist-info/entry_points.txt +0 -4
  58. /cicada/{dead_code_analyzer.py → dead_code/analyzer.py} +0 -0
  59. /cicada/{colors.py → format/colors.py} +0 -0
  60. {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/WHEEL +0 -0
  61. {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
  62. {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
- current_commit = {
381
- "sha": parts[0][:8],
382
- "full_sha": parts[0],
383
- "line_number": int(parts[2]),
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
- line_info = {**current_commit, "content": code_line}
403
- lines_data.append(line_info)
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"""