cicada-mcp 0.1.7__py3-none-any.whl → 0.2.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 (53) hide show
  1. cicada/ascii_art.py +60 -0
  2. cicada/clean.py +195 -60
  3. cicada/cli.py +757 -0
  4. cicada/colors.py +27 -0
  5. cicada/command_logger.py +14 -16
  6. cicada/dead_code_analyzer.py +12 -19
  7. cicada/extractors/__init__.py +6 -6
  8. cicada/extractors/base.py +3 -3
  9. cicada/extractors/call.py +11 -15
  10. cicada/extractors/dependency.py +39 -51
  11. cicada/extractors/doc.py +8 -9
  12. cicada/extractors/function.py +12 -24
  13. cicada/extractors/module.py +11 -15
  14. cicada/extractors/spec.py +8 -12
  15. cicada/find_dead_code.py +15 -39
  16. cicada/formatter.py +37 -91
  17. cicada/git_helper.py +22 -34
  18. cicada/indexer.py +122 -107
  19. cicada/interactive_setup.py +490 -0
  20. cicada/keybert_extractor.py +286 -0
  21. cicada/keyword_search.py +22 -30
  22. cicada/keyword_test.py +127 -0
  23. cicada/lightweight_keyword_extractor.py +5 -13
  24. cicada/mcp_entry.py +683 -0
  25. cicada/mcp_server.py +103 -209
  26. cicada/parser.py +9 -9
  27. cicada/pr_finder.py +15 -19
  28. cicada/pr_indexer/__init__.py +3 -3
  29. cicada/pr_indexer/cli.py +4 -9
  30. cicada/pr_indexer/github_api_client.py +22 -37
  31. cicada/pr_indexer/indexer.py +17 -29
  32. cicada/pr_indexer/line_mapper.py +8 -12
  33. cicada/pr_indexer/pr_index_builder.py +22 -34
  34. cicada/setup.py +189 -87
  35. cicada/utils/__init__.py +9 -9
  36. cicada/utils/call_site_formatter.py +4 -6
  37. cicada/utils/function_grouper.py +4 -4
  38. cicada/utils/hash_utils.py +12 -15
  39. cicada/utils/index_utils.py +15 -15
  40. cicada/utils/path_utils.py +24 -29
  41. cicada/utils/signature_builder.py +3 -3
  42. cicada/utils/subprocess_runner.py +17 -19
  43. cicada/utils/text_utils.py +1 -2
  44. cicada/version_check.py +2 -5
  45. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
  46. cicada_mcp-0.2.0.dist-info/RECORD +53 -0
  47. cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  48. cicada/install.py +0 -741
  49. cicada_mcp-0.1.7.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.7.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
cicada/mcp_server.py CHANGED
@@ -7,6 +7,7 @@ Provides an MCP tool to search for Elixir modules and their functions.
7
7
  Author: Cursor(Auto)
8
8
  """
9
9
 
10
+ import contextlib
10
11
  import os
11
12
  import sys
12
13
  import time
@@ -16,14 +17,14 @@ from typing import Any, cast
16
17
  import yaml
17
18
  from mcp.server import Server
18
19
  from mcp.server.stdio import stdio_server
19
- from mcp.types import Tool, TextContent
20
+ from mcp.types import TextContent, Tool
20
21
 
22
+ from cicada.command_logger import get_logger
21
23
  from cicada.formatter import ModuleFormatter
22
- from cicada.pr_finder import PRFinder
23
24
  from cicada.git_helper import GitHelper
24
- from cicada.utils import load_index, get_config_path, get_pr_index_path
25
25
  from cicada.mcp_tools import get_tool_definitions
26
- from cicada.command_logger import get_logger
26
+ from cicada.pr_finder import PRFinder
27
+ from cicada.utils import get_config_path, get_pr_index_path, load_index
27
28
 
28
29
 
29
30
  class CicadaServer:
@@ -97,25 +98,9 @@ class CicadaServer:
97
98
  if not repo_path:
98
99
  repo_path = str(Path.cwd().resolve())
99
100
 
100
- # Try new storage structure first
101
- try:
102
- config_path = get_config_path(repo_path)
103
- if config_path.exists():
104
- return str(config_path)
105
- except Exception as e:
106
- print(
107
- f"Warning: Could not load from new storage structure: {e}",
108
- file=sys.stderr,
109
- )
110
-
111
- # Fall back to old structure for backward compatibility
112
- old_path = Path(repo_path) / ".cicada" / "config.yaml"
113
- if old_path.exists():
114
- return str(old_path)
115
-
116
- # If neither exists, return the new storage path
117
- # (will trigger helpful error message in _load_config)
118
- return str(get_config_path(repo_path))
101
+ # Use new storage structure only
102
+ config_path = get_config_path(repo_path)
103
+ return str(config_path)
119
104
 
120
105
  def _load_config(self, config_path: str) -> dict:
121
106
  """Load configuration from YAML file."""
@@ -129,7 +114,7 @@ class CicadaServer:
129
114
  f" cicada vs # For VS Code"
130
115
  )
131
116
 
132
- with open(config_file, "r") as f:
117
+ with open(config_file) as f:
133
118
  data = yaml.safe_load(f)
134
119
  return data if isinstance(data, dict) else {}
135
120
 
@@ -158,9 +143,9 @@ class CicadaServer:
158
143
  f"Error: {e}\n\n"
159
144
  f"To rebuild the index, run:\n"
160
145
  f" cd {repo_path}\n"
161
- f" cicada-clean -f # Safer cleanup\n"
146
+ f" cicada clean -f # Safer cleanup\n"
162
147
  f" cicada cursor # or: cicada claude, cicada vs\n"
163
- )
148
+ ) from e
164
149
  except FileNotFoundError:
165
150
  raise FileNotFoundError(
166
151
  f"Index file not found: {index_path}\n\n"
@@ -168,7 +153,7 @@ class CicadaServer:
168
153
  f" cicada cursor # For Cursor\n"
169
154
  f" cicada claude # For Claude Code\n"
170
155
  f" cicada vs # For VS Code"
171
- )
156
+ ) from None
172
157
 
173
158
  @property
174
159
  def pr_index(self) -> dict[str, Any] | None:
@@ -177,25 +162,9 @@ class CicadaServer:
177
162
  # Get repo path from config
178
163
  repo_path = Path(self.config.get("repository", {}).get("path", "."))
179
164
 
180
- # Try new storage structure first
181
- try:
182
- pr_index_path = get_pr_index_path(repo_path)
183
- if pr_index_path.exists():
184
- self._pr_index = load_index(
185
- pr_index_path, verbose=True, raise_on_error=False
186
- )
187
- return self._pr_index
188
- except Exception as e:
189
- print(
190
- f"Warning: Could not load PR index from new storage structure: {e}",
191
- file=sys.stderr,
192
- )
193
-
194
- # Fall back to old structure for backward compatibility
195
- pr_index_path = repo_path / ".cicada" / "pr_index.json"
196
- self._pr_index = load_index(
197
- pr_index_path, verbose=True, raise_on_error=False
198
- )
165
+ # Use new storage structure only
166
+ pr_index_path = get_pr_index_path(repo_path)
167
+ self._pr_index = load_index(pr_index_path, verbose=True, raise_on_error=False)
199
168
  return self._pr_index
200
169
 
201
170
  def _load_pr_index(self) -> dict[str, Any] | None:
@@ -203,19 +172,8 @@ class CicadaServer:
203
172
  # Get repo path from config
204
173
  repo_path = Path(self.config.get("repository", {}).get("path", "."))
205
174
 
206
- # Try new storage structure first
207
- try:
208
- pr_index_path = get_pr_index_path(repo_path)
209
- if pr_index_path.exists():
210
- return load_index(pr_index_path, verbose=True, raise_on_error=False)
211
- except Exception as e:
212
- print(
213
- f"Warning: Could not load PR index from new storage structure: {e}",
214
- file=sys.stderr,
215
- )
216
-
217
- # Fall back to old structure for backward compatibility
218
- pr_index_path = repo_path / ".cicada" / "pr_index.json"
175
+ # Use new storage structure only
176
+ pr_index_path = get_pr_index_path(repo_path)
219
177
  return load_index(pr_index_path, verbose=True, raise_on_error=False)
220
178
 
221
179
  def _check_keywords_available(self) -> bool:
@@ -239,9 +197,7 @@ class CicadaServer:
239
197
  """List available MCP tools."""
240
198
  return get_tool_definitions()
241
199
 
242
- async def call_tool_with_logging(
243
- self, name: str, arguments: dict
244
- ) -> list[TextContent]:
200
+ async def call_tool_with_logging(self, name: str, arguments: dict) -> list[TextContent]:
245
201
  """Wrapper for call_tool that logs execution details."""
246
202
  from datetime import datetime
247
203
 
@@ -296,9 +252,7 @@ class CicadaServer:
296
252
  module_name = resolved_module
297
253
 
298
254
  assert module_name is not None, "module_name must be provided"
299
- return await self._search_module(
300
- module_name, output_format, private_functions
301
- )
255
+ return await self._search_module(module_name, output_format, private_functions)
302
256
  elif name == "search_function":
303
257
  function_name = arguments.get("function_name")
304
258
  output_format = arguments.get("format", "markdown")
@@ -354,10 +308,9 @@ class CicadaServer:
354
308
  return [TextContent(type="text", text=error_msg)]
355
309
 
356
310
  # Validate line range parameters
357
- if precise_tracking or show_evolution:
358
- if not start_line or not end_line:
359
- error_msg = "Both 'start_line' and 'end_line' are required for precise_tracking or show_evolution"
360
- return [TextContent(type="text", text=error_msg)]
311
+ if (precise_tracking or show_evolution) and (not start_line or not end_line):
312
+ error_msg = "Both 'start_line' and 'end_line' are required for precise_tracking or show_evolution"
313
+ return [TextContent(type="text", text=error_msg)]
361
314
 
362
315
  return await self._get_file_history(
363
316
  file_path,
@@ -447,9 +400,7 @@ class CicadaServer:
447
400
  data = self.index["modules"][module_name]
448
401
 
449
402
  if output_format == "json":
450
- result = ModuleFormatter.format_module_json(
451
- module_name, data, private_functions
452
- )
403
+ result = ModuleFormatter.format_module_json(module_name, data, private_functions)
453
404
  else:
454
405
  result = ModuleFormatter.format_module_markdown(
455
406
  module_name, data, private_functions
@@ -463,9 +414,7 @@ class CicadaServer:
463
414
  if output_format == "json":
464
415
  error_result = ModuleFormatter.format_error_json(module_name, total_modules)
465
416
  else:
466
- error_result = ModuleFormatter.format_error_markdown(
467
- module_name, total_modules
468
- )
417
+ error_result = ModuleFormatter.format_error_markdown(module_name, total_modules)
469
418
 
470
419
  return [TextContent(type="text", text=error_result)]
471
420
 
@@ -497,10 +446,8 @@ class CicadaServer:
497
446
  if "/" in target_name:
498
447
  parts = target_name.split("/")
499
448
  target_name = parts[0]
500
- try:
449
+ with contextlib.suppress(ValueError, IndexError):
501
450
  target_arity = int(parts[1])
502
- except (ValueError, IndexError):
503
- pass
504
451
 
505
452
  # Search across all modules for function definitions
506
453
  results = []
@@ -511,51 +458,46 @@ class CicadaServer:
511
458
 
512
459
  for func in module_data["functions"]:
513
460
  # Match by name and optionally arity
514
- if func["name"] == target_name:
515
- if target_arity is None or func["arity"] == target_arity:
516
- # Find call sites for this function
517
- call_sites = self._find_call_sites(
518
- target_module=module_name,
519
- target_function=target_name,
520
- target_arity=func["arity"],
521
- )
461
+ if func["name"] == target_name and (
462
+ target_arity is None or func["arity"] == target_arity
463
+ ):
464
+ # Find call sites for this function
465
+ call_sites = self._find_call_sites(
466
+ target_module=module_name,
467
+ target_function=target_name,
468
+ target_arity=func["arity"],
469
+ )
522
470
 
523
- # Filter for test files only if requested
524
- if test_files_only:
525
- call_sites = self._filter_test_call_sites(call_sites)
526
-
527
- # Optionally include usage examples (actual code lines)
528
- call_sites_with_examples = []
529
- if include_usage_examples and call_sites:
530
- # Consolidate call sites by calling module (one example per module)
531
- consolidated_sites = self._consolidate_call_sites_by_module(
532
- call_sites
533
- )
534
- # Limit the number of examples
535
- call_sites_with_examples = consolidated_sites[:max_examples]
536
- # Extract code lines for each call site
537
- self._add_code_examples(call_sites_with_examples)
538
-
539
- results.append(
540
- {
541
- "module": module_name,
542
- "moduledoc": module_data.get("moduledoc"),
543
- "function": func,
544
- "file": module_data["file"],
545
- "call_sites": call_sites,
546
- "call_sites_with_examples": call_sites_with_examples,
547
- }
548
- )
471
+ # Filter for test files only if requested
472
+ if test_files_only:
473
+ call_sites = self._filter_test_call_sites(call_sites)
474
+
475
+ # Optionally include usage examples (actual code lines)
476
+ call_sites_with_examples = []
477
+ if include_usage_examples and call_sites:
478
+ # Consolidate call sites by calling module (one example per module)
479
+ consolidated_sites = self._consolidate_call_sites_by_module(call_sites)
480
+ # Limit the number of examples
481
+ call_sites_with_examples = consolidated_sites[:max_examples]
482
+ # Extract code lines for each call site
483
+ self._add_code_examples(call_sites_with_examples)
484
+
485
+ results.append(
486
+ {
487
+ "module": module_name,
488
+ "moduledoc": module_data.get("moduledoc"),
489
+ "function": func,
490
+ "file": module_data["file"],
491
+ "call_sites": call_sites,
492
+ "call_sites_with_examples": call_sites_with_examples,
493
+ }
494
+ )
549
495
 
550
496
  # Format results
551
497
  if output_format == "json":
552
- result = ModuleFormatter.format_function_results_json(
553
- function_name, results
554
- )
498
+ result = ModuleFormatter.format_function_results_json(function_name, results)
555
499
  else:
556
- result = ModuleFormatter.format_function_results_markdown(
557
- function_name, results
558
- )
500
+ result = ModuleFormatter.format_function_results_markdown(function_name, results)
559
501
 
560
502
  return [TextContent(type="text", text=result)]
561
503
 
@@ -666,9 +608,7 @@ class CicadaServer:
666
608
  "arity": call["arity"],
667
609
  "lines": [],
668
610
  "alias_used": (
669
- call_module
670
- if call_module != resolved_module
671
- else None
611
+ call_module if call_module != resolved_module else None
672
612
  ),
673
613
  }
674
614
 
@@ -686,13 +626,9 @@ class CicadaServer:
686
626
 
687
627
  # Format results
688
628
  if output_format == "json":
689
- result = ModuleFormatter.format_module_usage_json(
690
- module_name, usage_results
691
- )
629
+ result = ModuleFormatter.format_module_usage_json(module_name, usage_results)
692
630
  else:
693
- result = ModuleFormatter.format_module_usage_markdown(
694
- module_name, usage_results
695
- )
631
+ result = ModuleFormatter.format_module_usage_markdown(module_name, usage_results)
696
632
 
697
633
  return [TextContent(type="text", text=result)]
698
634
 
@@ -724,14 +660,14 @@ class CicadaServer:
724
660
 
725
661
  try:
726
662
  # Read all lines from the file
727
- with open(file_path, "r") as f:
663
+ with open(file_path) as f:
728
664
  lines = f.readlines()
729
665
 
730
666
  # Extract complete function call
731
667
  code_lines = self._extract_complete_call(lines, line_number)
732
668
  if code_lines:
733
669
  site["code_line"] = code_lines
734
- except (FileNotFoundError, IOError, IndexError):
670
+ except (OSError, FileNotFoundError, IndexError):
735
671
  # If we can't read the file/line, just skip adding the code example
736
672
  pass
737
673
 
@@ -784,9 +720,7 @@ class CicadaServer:
784
720
 
785
721
  return "\n".join(extracted_lines) if extracted_lines else None
786
722
 
787
- def _find_call_sites(
788
- self, target_module: str, target_function: str, target_arity: int
789
- ) -> list:
723
+ def _find_call_sites(self, target_module: str, target_function: str, target_arity: int) -> list:
790
724
  """
791
725
  Find all locations where a function is called.
792
726
 
@@ -828,16 +762,11 @@ class CicadaServer:
828
762
  if caller_module == target_module:
829
763
  # Filter out calls that are part of the function definition
830
764
  # (@spec, @doc appear 1-5 lines before the def)
831
- if (
832
- function_def_line
833
- and abs(call["line"] - function_def_line) <= 5
834
- ):
765
+ if function_def_line and abs(call["line"] - function_def_line) <= 5:
835
766
  continue
836
767
 
837
768
  # Find the calling function
838
- calling_function = self._find_function_at_line(
839
- caller_module, call["line"]
840
- )
769
+ calling_function = self._find_function_at_line(caller_module, call["line"])
841
770
 
842
771
  call_sites.append(
843
772
  {
@@ -855,9 +784,7 @@ class CicadaServer:
855
784
  # Check if this resolves to our target module
856
785
  if resolved_module == target_module:
857
786
  # Find the calling function
858
- calling_function = self._find_function_at_line(
859
- caller_module, call["line"]
860
- )
787
+ calling_function = self._find_function_at_line(caller_module, call["line"])
861
788
 
862
789
  call_sites.append(
863
790
  {
@@ -867,9 +794,7 @@ class CicadaServer:
867
794
  "line": call["line"],
868
795
  "call_type": "qualified",
869
796
  "alias_used": (
870
- call_module
871
- if call_module != resolved_module
872
- else None
797
+ call_module if call_module != resolved_module else None
873
798
  ),
874
799
  }
875
800
  )
@@ -898,14 +823,13 @@ class CicadaServer:
898
823
  for func in functions:
899
824
  func_line = func["line"]
900
825
  # The function must be defined before or at the line
901
- if func_line <= line:
902
- # Keep the closest one
903
- if best_match is None or func_line > best_match["line"]:
904
- best_match = {
905
- "name": func["name"],
906
- "arity": func["arity"],
907
- "line": func_line,
908
- }
826
+ # Keep the closest one
827
+ if func_line <= line and (best_match is None or func_line > best_match["line"]):
828
+ best_match = {
829
+ "name": func["name"],
830
+ "arity": func["arity"],
831
+ "line": func_line,
832
+ }
909
833
 
910
834
  return best_match
911
835
 
@@ -964,14 +888,14 @@ class CicadaServer:
964
888
  try:
965
889
  # Get repo path from config
966
890
  repo_path = self.config.get("repository", {}).get("path", ".")
967
- index_path = Path(repo_path) / ".cicada" / "pr_index.json"
891
+ index_path = get_pr_index_path(repo_path)
968
892
 
969
893
  # Check if index exists
970
894
  if not index_path.exists():
971
895
  error_msg = (
972
896
  "PR index not found. Please run:\n"
973
- " cicada-index-pr\n\n"
974
- "This will create the PR index at .cicada/pr_index.json"
897
+ " cicada index-pr\n\n"
898
+ f"This will create the PR index at {index_path}"
975
899
  )
976
900
  return [TextContent(type="text", text=error_msg)]
977
901
 
@@ -979,7 +903,7 @@ class CicadaServer:
979
903
  pr_finder = PRFinder(
980
904
  repo_path=repo_path,
981
905
  use_index=True,
982
- index_path=".cicada/pr_index.json",
906
+ index_path=str(index_path),
983
907
  verbose=False,
984
908
  )
985
909
 
@@ -994,15 +918,13 @@ class CicadaServer:
994
918
  use_index=False,
995
919
  verbose=False,
996
920
  )
997
- network_result = pr_finder_network.find_pr_for_line(
998
- file_path, line_number
999
- )
921
+ network_result = pr_finder_network.find_pr_for_line(file_path, line_number)
1000
922
 
1001
923
  if network_result.get("pr") is not None:
1002
924
  # PR exists but not in index - suggest update
1003
925
  error_msg = (
1004
926
  "PR index is incomplete. Please run:\n"
1005
- " cicada-index-pr\n\n"
927
+ " cicada index-pr\n\n"
1006
928
  "This will update the index with recent PRs (incremental by default)."
1007
929
  )
1008
930
  return [TextContent(type="text", text=error_msg)]
@@ -1052,9 +974,7 @@ class CicadaServer:
1052
974
  - Requires .gitattributes with "*.ex diff=elixir" for function tracking
1053
975
  """
1054
976
  if not self.git_helper:
1055
- error_msg = (
1056
- "Git history is not available (repository may not be a git repo)"
1057
- )
977
+ error_msg = "Git history is not available (repository may not be a git repo)"
1058
978
  return [TextContent(type="text", text=error_msg)]
1059
979
 
1060
980
  try:
@@ -1117,9 +1037,7 @@ class CicadaServer:
1117
1037
  "*Using function tracking (git log -L :funcname:file) - tracks function even as it moves*\n"
1118
1038
  )
1119
1039
  elif tracking_method == "line":
1120
- lines.append(
1121
- "*Using line-based tracking (git log -L start,end:file)*\n"
1122
- )
1040
+ lines.append("*Using line-based tracking (git log -L start,end:file)*\n")
1123
1041
 
1124
1042
  # Add evolution metadata if available
1125
1043
  if evolution:
@@ -1139,9 +1057,7 @@ class CicadaServer:
1139
1057
 
1140
1058
  if evolution.get("modification_frequency"):
1141
1059
  freq = evolution["modification_frequency"]
1142
- lines.append(
1143
- f"- **Modification Frequency:** {freq:.2f} commits/month"
1144
- )
1060
+ lines.append(f"- **Modification Frequency:** {freq:.2f} commits/month")
1145
1061
 
1146
1062
  lines.append("") # Empty line
1147
1063
 
@@ -1150,16 +1066,12 @@ class CicadaServer:
1150
1066
  for i, commit in enumerate(commits, 1):
1151
1067
  lines.append(f"## {i}. {commit['summary']}")
1152
1068
  lines.append(f"- **Commit:** `{commit['sha']}`")
1153
- lines.append(
1154
- f"- **Author:** {commit['author']} ({commit['author_email']})"
1155
- )
1069
+ lines.append(f"- **Author:** {commit['author']} ({commit['author_email']})")
1156
1070
  lines.append(f"- **Date:** {commit['date']}")
1157
1071
 
1158
1072
  # Add relevance indicator for function searches
1159
1073
  if "relevance" in commit:
1160
- relevance_emoji = (
1161
- "🎯" if commit["relevance"] == "mentioned" else "📝"
1162
- )
1074
+ relevance_emoji = "🎯" if commit["relevance"] == "mentioned" else "📝"
1163
1075
  relevance_text = (
1164
1076
  "Function mentioned"
1165
1077
  if commit["relevance"] == "mentioned"
@@ -1199,9 +1111,7 @@ class CicadaServer:
1199
1111
  return [TextContent(type="text", text=error_msg)]
1200
1112
 
1201
1113
  try:
1202
- blame_groups = self.git_helper.get_function_history(
1203
- file_path, start_line, end_line
1204
- )
1114
+ blame_groups = self.git_helper.get_function_history(file_path, start_line, end_line)
1205
1115
 
1206
1116
  if not blame_groups:
1207
1117
  result = f"No blame information found for {file_path} lines {start_line}-{end_line}"
@@ -1220,9 +1130,7 @@ class CicadaServer:
1220
1130
  )
1221
1131
  lines.append(f"## Group {i}: {group['author']} ({line_range})")
1222
1132
 
1223
- lines.append(
1224
- f"- **Author:** {group['author']} ({group['author_email']})"
1225
- )
1133
+ lines.append(f"- **Author:** {group['author']} ({group['author_email']})")
1226
1134
  lines.append(f"- **Commit:** `{group['sha']}`")
1227
1135
  lines.append(f"- **Date:** {group['date'][:10]}")
1228
1136
  lines.append(f"- **Lines:** {group['line_count']}\n")
@@ -1255,7 +1163,7 @@ class CicadaServer:
1255
1163
  if not self.pr_index:
1256
1164
  error_msg = (
1257
1165
  "PR index not available. Please run:\n"
1258
- " python cicada/pr_indexer.py\n\n"
1166
+ " cicada index-pr\n\n"
1259
1167
  "This will create the PR index at .cicada/pr_index.json"
1260
1168
  )
1261
1169
  return [TextContent(type="text", text=error_msg)]
@@ -1268,9 +1176,7 @@ class CicadaServer:
1268
1176
  try:
1269
1177
  file_path_obj = file_path_obj.relative_to(repo_path)
1270
1178
  except ValueError:
1271
- error_msg = (
1272
- f"File path {file_path} is not within repository {repo_path}"
1273
- )
1179
+ error_msg = f"File path {file_path} is not within repository {repo_path}"
1274
1180
  return [TextContent(type="text", text=error_msg)]
1275
1181
 
1276
1182
  file_path_str = str(file_path_obj)
@@ -1310,9 +1216,7 @@ class CicadaServer:
1310
1216
  if len(desc_lines) > 10:
1311
1217
  trimmed_desc = "\n".join(desc_lines[:10])
1312
1218
  lines.append(f"{trimmed_desc}")
1313
- lines.append(
1314
- f"\n*... (trimmed, {len(desc_lines) - 10} more lines)*\n"
1315
- )
1219
+ lines.append(f"\n*... (trimmed, {len(desc_lines) - 10} more lines)*\n")
1316
1220
  else:
1317
1221
  lines.append(f"{description}\n")
1318
1222
 
@@ -1368,8 +1272,9 @@ class CicadaServer:
1368
1272
  if not self._has_keywords:
1369
1273
  error_msg = (
1370
1274
  "No keywords found in index. Please rebuild the index with keyword extraction:\n\n"
1371
- " cicada-index --extract-keywords\n\n"
1372
- "This will extract keywords from documentation using NLP."
1275
+ " cicada index --nlp # NLP-based extraction (lemminflect)\n"
1276
+ " cicada index --rag # BERT-based extraction\n\n"
1277
+ "This will extract keywords from documentation for semantic search."
1373
1278
  )
1374
1279
  return [TextContent(type="text", text=error_msg)]
1375
1280
 
@@ -1384,15 +1289,11 @@ class CicadaServer:
1384
1289
  # Format results
1385
1290
  from cicada.formatter import ModuleFormatter
1386
1291
 
1387
- formatted_result = ModuleFormatter.format_keyword_search_results_markdown(
1388
- keywords, results
1389
- )
1292
+ formatted_result = ModuleFormatter.format_keyword_search_results_markdown(keywords, results)
1390
1293
 
1391
1294
  return [TextContent(type="text", text=formatted_result)]
1392
1295
 
1393
- async def _find_dead_code(
1394
- self, min_confidence: str, output_format: str
1395
- ) -> list[TextContent]:
1296
+ async def _find_dead_code(self, min_confidence: str, output_format: str) -> list[TextContent]:
1396
1297
  """
1397
1298
  Find potentially unused public functions.
1398
1299
 
@@ -1406,8 +1307,8 @@ class CicadaServer:
1406
1307
  from cicada.dead_code_analyzer import DeadCodeAnalyzer
1407
1308
  from cicada.find_dead_code import (
1408
1309
  filter_by_confidence,
1409
- format_markdown,
1410
1310
  format_json,
1311
+ format_markdown,
1411
1312
  )
1412
1313
 
1413
1314
  # Run analysis
@@ -1418,10 +1319,7 @@ class CicadaServer:
1418
1319
  results = filter_by_confidence(results, min_confidence)
1419
1320
 
1420
1321
  # Format output
1421
- if output_format == "json":
1422
- output = format_json(results)
1423
- else:
1424
- output = format_markdown(results)
1322
+ output = format_json(results) if output_format == "json" else format_markdown(results)
1425
1323
 
1426
1324
  return [TextContent(type="text", text=output)]
1427
1325
 
@@ -1459,13 +1357,12 @@ def _auto_setup_if_needed():
1459
1357
  This enables zero-config MCP usage - just point the MCP config to cicada-server
1460
1358
  and it will index the repository on first run.
1461
1359
  """
1360
+ from cicada.setup import create_config_yaml, index_repository
1462
1361
  from cicada.utils import (
1362
+ create_storage_dir,
1463
1363
  get_config_path,
1464
1364
  get_index_path,
1465
- create_storage_dir,
1466
- get_storage_dir,
1467
1365
  )
1468
- from cicada.setup import index_repository, create_config_yaml
1469
1366
 
1470
1367
  # Determine repository path from environment or current directory
1471
1368
  repo_path_str = os.environ.get("CICADA_REPO_PATH")
@@ -1483,10 +1380,7 @@ def _auto_setup_if_needed():
1483
1380
  else workspace_paths
1484
1381
  )
1485
1382
 
1486
- if repo_path_str:
1487
- repo_path = Path(repo_path_str).resolve()
1488
- else:
1489
- repo_path = Path.cwd().resolve()
1383
+ repo_path = Path(repo_path_str).resolve() if repo_path_str else Path.cwd().resolve()
1490
1384
 
1491
1385
  # Check if config and index already exist
1492
1386
  config_path = get_config_path(repo_path)
cicada/parser.py CHANGED
@@ -7,22 +7,22 @@ Author: Cursor(Auto)
7
7
  """
8
8
 
9
9
  import tree_sitter_elixir as ts_elixir
10
- from tree_sitter import Parser, Language
10
+ from tree_sitter import Language, Parser
11
11
 
12
12
  from .extractors import (
13
- extract_modules,
14
- extract_functions,
15
- extract_specs,
16
- match_specs_to_functions,
17
- extract_docs,
18
- match_docs_to_functions,
19
13
  extract_aliases,
14
+ extract_behaviours,
15
+ extract_docs,
16
+ extract_function_calls,
17
+ extract_functions,
20
18
  extract_imports,
19
+ extract_modules,
21
20
  extract_requires,
21
+ extract_specs,
22
22
  extract_uses,
23
- extract_behaviours,
24
- extract_function_calls,
25
23
  extract_value_mentions,
24
+ match_docs_to_functions,
25
+ match_specs_to_functions,
26
26
  )
27
27
 
28
28