cicada-mcp 0.1.5__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.
- cicada/ascii_art.py +60 -0
- cicada/clean.py +195 -60
- cicada/cli.py +757 -0
- cicada/colors.py +27 -0
- cicada/command_logger.py +14 -16
- cicada/dead_code_analyzer.py +12 -19
- cicada/extractors/__init__.py +6 -6
- cicada/extractors/base.py +3 -3
- cicada/extractors/call.py +11 -15
- cicada/extractors/dependency.py +39 -51
- cicada/extractors/doc.py +8 -9
- cicada/extractors/function.py +12 -24
- cicada/extractors/module.py +11 -15
- cicada/extractors/spec.py +8 -12
- cicada/find_dead_code.py +15 -39
- cicada/formatter.py +37 -91
- cicada/git_helper.py +22 -34
- cicada/indexer.py +165 -132
- cicada/interactive_setup.py +490 -0
- cicada/keybert_extractor.py +286 -0
- cicada/keyword_search.py +22 -30
- cicada/keyword_test.py +127 -0
- cicada/lightweight_keyword_extractor.py +5 -13
- cicada/mcp_entry.py +683 -0
- cicada/mcp_server.py +110 -232
- cicada/parser.py +9 -9
- cicada/pr_finder.py +15 -19
- cicada/pr_indexer/__init__.py +3 -3
- cicada/pr_indexer/cli.py +4 -9
- cicada/pr_indexer/github_api_client.py +22 -37
- cicada/pr_indexer/indexer.py +17 -29
- cicada/pr_indexer/line_mapper.py +8 -12
- cicada/pr_indexer/pr_index_builder.py +22 -34
- cicada/setup.py +198 -89
- cicada/utils/__init__.py +9 -9
- cicada/utils/call_site_formatter.py +4 -6
- cicada/utils/function_grouper.py +4 -4
- cicada/utils/hash_utils.py +12 -15
- cicada/utils/index_utils.py +15 -15
- cicada/utils/path_utils.py +24 -29
- cicada/utils/signature_builder.py +3 -3
- cicada/utils/subprocess_runner.py +17 -19
- cicada/utils/text_utils.py +1 -2
- cicada/version_check.py +2 -5
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
- cicada_mcp-0.2.0.dist-info/RECORD +53 -0
- cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
- cicada/install.py +0 -741
- cicada_mcp-0.1.5.dist-info/RECORD +0 -47
- cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.1.5.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
|
|
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.
|
|
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
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
#
|
|
207
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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 =
|
|
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
|
|
974
|
-
"This will create the PR index at
|
|
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=
|
|
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
|
|
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
|
-
"
|
|
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
|
|
1372
|
-
"
|
|
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)
|
|
@@ -1496,43 +1390,27 @@ def _auto_setup_if_needed():
|
|
|
1496
1390
|
# Already set up, nothing to do
|
|
1497
1391
|
return
|
|
1498
1392
|
|
|
1499
|
-
# Setup needed - create storage and index
|
|
1500
|
-
print("=" * 60, file=sys.stderr)
|
|
1501
|
-
print("Cicada: First-time setup detected", file=sys.stderr)
|
|
1502
|
-
print("=" * 60, file=sys.stderr)
|
|
1503
|
-
print(file=sys.stderr)
|
|
1504
|
-
|
|
1393
|
+
# Setup needed - create storage and index (silent mode)
|
|
1505
1394
|
# Validate it's an Elixir project
|
|
1506
1395
|
if not (repo_path / "mix.exs").exists():
|
|
1507
1396
|
print(
|
|
1508
|
-
f"Error: {repo_path} does not appear to be an Elixir project",
|
|
1397
|
+
f"Error: {repo_path} does not appear to be an Elixir project (mix.exs not found)",
|
|
1509
1398
|
file=sys.stderr,
|
|
1510
1399
|
)
|
|
1511
|
-
print("(mix.exs not found)", file=sys.stderr)
|
|
1512
1400
|
sys.exit(1)
|
|
1513
1401
|
|
|
1514
1402
|
try:
|
|
1515
1403
|
# Create storage directory
|
|
1516
1404
|
storage_dir = create_storage_dir(repo_path)
|
|
1517
|
-
print(f"Repository: {repo_path}", file=sys.stderr)
|
|
1518
|
-
print(f"Storage: {storage_dir}", file=sys.stderr)
|
|
1519
|
-
print(file=sys.stderr)
|
|
1520
|
-
|
|
1521
|
-
# Index repository
|
|
1522
|
-
index_repository(repo_path)
|
|
1523
|
-
print(file=sys.stderr)
|
|
1524
1405
|
|
|
1525
|
-
#
|
|
1526
|
-
|
|
1527
|
-
print(file=sys.stderr)
|
|
1406
|
+
# Index repository (silent mode)
|
|
1407
|
+
index_repository(repo_path, verbose=False)
|
|
1528
1408
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
print("=" * 60, file=sys.stderr)
|
|
1532
|
-
print(file=sys.stderr)
|
|
1409
|
+
# Create config.yaml (silent mode)
|
|
1410
|
+
create_config_yaml(repo_path, storage_dir, verbose=False)
|
|
1533
1411
|
|
|
1534
1412
|
except Exception as e:
|
|
1535
|
-
print(f"
|
|
1413
|
+
print(f"Cicada auto-setup error: {e}", file=sys.stderr)
|
|
1536
1414
|
sys.exit(1)
|
|
1537
1415
|
|
|
1538
1416
|
|