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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Helper functions for interactive setup - non-user interactive code."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from cicada.format import BOLD, GREEN, GREY, PRIMARY, RESET
|
|
9
|
+
from cicada.setup import EditorType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotElixirProjectError(Exception):
|
|
13
|
+
"""Raised when the given path is not an Elixir project."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Tier configuration data
|
|
17
|
+
_TIER_OPTIONS = (
|
|
18
|
+
("Fast - Term frequency + inflections (no downloads)", ("regular", "lemmi")),
|
|
19
|
+
("Balanced - KeyBERT + GloVe semantic expansion (261MB)", ("bert", "glove")),
|
|
20
|
+
("Maximum - KeyBERT + FastText expansion (1091MB)", ("bert", "fasttext")),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
TIER_ITEMS = [label for label, _ in _TIER_OPTIONS]
|
|
24
|
+
TIER_MAP = {idx: methods for idx, (_, methods) in enumerate(_TIER_OPTIONS)}
|
|
25
|
+
TIER_MAP_TEXT = {str(idx + 1): methods for idx, methods in TIER_MAP.items()}
|
|
26
|
+
|
|
27
|
+
# Editor configuration data
|
|
28
|
+
_EDITOR_OPTIONS = (
|
|
29
|
+
("Claude Code - AI-powered code editor", "claude"),
|
|
30
|
+
("Cursor - AI-first code editor", "cursor"),
|
|
31
|
+
("VS Code - Visual Studio Code", "vs"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
EDITOR_ITEMS = [label for label, _ in _EDITOR_OPTIONS]
|
|
35
|
+
EDITOR_MAP = {idx: value for idx, (_, value) in enumerate(_EDITOR_OPTIONS)}
|
|
36
|
+
EDITOR_MAP_TEXT = {str(idx + 1): value for idx, value in EDITOR_MAP.items()}
|
|
37
|
+
|
|
38
|
+
# PR indexing options
|
|
39
|
+
PR_ITEMS = [
|
|
40
|
+
"No - Skip PR indexing (can run later with 'cicada-pr-indexer')",
|
|
41
|
+
"Yes - Index PRs now (requires GitHub access)",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# CLAUDE.md options
|
|
45
|
+
CLAUDE_MD_ITEMS = [
|
|
46
|
+
"Yes - Add Cicada usage guide to CLAUDE.md (recommended)",
|
|
47
|
+
"No - Skip CLAUDE.md setup",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# CLAUDE.md documentation content
|
|
51
|
+
CICADA_DOCS = """
|
|
52
|
+
<cicada>
|
|
53
|
+
**ALWAYS use cicada-mcp tools for Elixir code searches. NEVER use Grep/Find for these tasks.**
|
|
54
|
+
|
|
55
|
+
### Use cicada tools for:
|
|
56
|
+
- PREFERRED for Elixir: View a module's complete API - functions with arity, signatures, docs, typespecs, and line numbers. `mcp__cicada__search_module`
|
|
57
|
+
- PREFERRED for Elixir: Find function definitions and call sites across the codebase. `mcp__cicada__search_function`
|
|
58
|
+
- PREFERRED for Elixir: Find all module usage and dependencies for impact analysis. `mcp__cicada__search_module_usage`
|
|
59
|
+
- PREFERRED for git history: Discover why code exists and who wrote it. `mcp__cicada__find_pr_for_line`
|
|
60
|
+
- PREFERRED for git history: Get commit log for files or functions. `mcp__cicada__get_commit_history`
|
|
61
|
+
- PREFERRED for authorship: Git blame showing who wrote each line. `mcp__cicada__get_blame`
|
|
62
|
+
- Get all PRs that modified a file with descriptions and review comments. `mcp__cicada__get_file_pr_history`
|
|
63
|
+
- Semantic search for code by concept/topic when exact names are unknown. `mcp__cicada__search_by_keywords`
|
|
64
|
+
- Find potentially unused public functions with confidence levels. `mcp__cicada__find_dead_code`
|
|
65
|
+
|
|
66
|
+
### DO NOT use Grep for:
|
|
67
|
+
- ❌ Searching for module structure
|
|
68
|
+
- ❌ Searching for function definitions
|
|
69
|
+
- ❌ Searching for module imports/usage
|
|
70
|
+
|
|
71
|
+
### You can still use Grep for:
|
|
72
|
+
- ✓ Non-code files (markdown, JSON, config)
|
|
73
|
+
- ✓ String literal searches
|
|
74
|
+
- ✓ Pattern matching in single line comments
|
|
75
|
+
</cicada>
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def display_tier_selection(tier_index: int) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Display confirmation message for tier selection.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
tier_index: The selected tier index (0, 1, or 2)
|
|
85
|
+
"""
|
|
86
|
+
print()
|
|
87
|
+
if tier_index == 0:
|
|
88
|
+
print(f"{GREEN}✓{RESET} Selected: FAST tier")
|
|
89
|
+
print(" Term frequency extraction + inflections")
|
|
90
|
+
print(" Fast, lightweight, no model downloads")
|
|
91
|
+
elif tier_index == 1:
|
|
92
|
+
print(f"{GREEN}✓{RESET} Selected: BALANCED tier")
|
|
93
|
+
print(" KeyBERT semantic extraction (133MB)")
|
|
94
|
+
print(" GloVe semantic expansion (128MB)")
|
|
95
|
+
print(" Total: 261MB download")
|
|
96
|
+
else: # tier_index == 2
|
|
97
|
+
print(f"{GREEN}✓{RESET} Selected: MAXIMUM tier")
|
|
98
|
+
print(" KeyBERT semantic extraction (133MB)")
|
|
99
|
+
print(" FastText semantic expansion (958MB)")
|
|
100
|
+
print(" Total: 1091MB download")
|
|
101
|
+
print()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def display_pr_indexing_selection(index_prs: bool) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Display confirmation message for PR indexing selection.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
index_prs: Whether to index PRs
|
|
110
|
+
"""
|
|
111
|
+
print()
|
|
112
|
+
if index_prs:
|
|
113
|
+
print(f"{GREEN}✓{RESET} Will index pull requests")
|
|
114
|
+
else:
|
|
115
|
+
print(f"{GREEN}✓{RESET} Skipping PR indexing")
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def display_claude_md_selection(add_to_claude_md: bool) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Display confirmation message for CLAUDE.md selection.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
add_to_claude_md: Whether to add to CLAUDE.md
|
|
125
|
+
"""
|
|
126
|
+
print()
|
|
127
|
+
if add_to_claude_md:
|
|
128
|
+
print(f"{GREEN}✓{RESET} Will add Cicada guide to CLAUDE.md")
|
|
129
|
+
else:
|
|
130
|
+
print(f"{GREEN}✓{RESET} Skipping CLAUDE.md setup")
|
|
131
|
+
print()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def display_editor_selection(editor: str) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Display confirmation message for editor selection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
editor: The selected editor ('claude', 'cursor', or 'vs')
|
|
140
|
+
"""
|
|
141
|
+
print()
|
|
142
|
+
print(f"{GREEN}✓{RESET} Selected: {editor.upper()}")
|
|
143
|
+
print()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_existing_config(repo_path: Path) -> tuple[str, str] | None:
|
|
147
|
+
"""
|
|
148
|
+
Read existing configuration from the repository if it exists.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
repo_path: Path to the repository
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tuple of (extraction_method, expansion_method) if config exists, None otherwise
|
|
155
|
+
"""
|
|
156
|
+
from cicada.utils.storage import get_config_path, get_index_path
|
|
157
|
+
|
|
158
|
+
config_path = get_config_path(repo_path)
|
|
159
|
+
index_path = get_index_path(repo_path)
|
|
160
|
+
|
|
161
|
+
if not (config_path.exists() and index_path.exists()):
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with open(config_path) as f:
|
|
166
|
+
existing_config = yaml.safe_load(f)
|
|
167
|
+
extraction_method = existing_config.get("keyword_extraction", {}).get(
|
|
168
|
+
"method", "regular"
|
|
169
|
+
)
|
|
170
|
+
expansion_method = existing_config.get("keyword_expansion", {}).get("method", "lemmi")
|
|
171
|
+
return (extraction_method, expansion_method)
|
|
172
|
+
except Exception:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def run_setup(
|
|
177
|
+
editor: str,
|
|
178
|
+
repo_path: Path,
|
|
179
|
+
extraction_method: str,
|
|
180
|
+
expansion_method: str,
|
|
181
|
+
index_exists: bool = False,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Run the setup.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
editor: The selected editor
|
|
188
|
+
repo_path: Path to the repository
|
|
189
|
+
extraction_method: Keyword extraction method
|
|
190
|
+
expansion_method: Keyword expansion method
|
|
191
|
+
index_exists: Whether the index already exists
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
Exception: If setup fails
|
|
195
|
+
"""
|
|
196
|
+
from cicada.setup import setup
|
|
197
|
+
|
|
198
|
+
setup(
|
|
199
|
+
cast(EditorType, editor),
|
|
200
|
+
repo_path,
|
|
201
|
+
extraction_method=extraction_method,
|
|
202
|
+
expansion_method=expansion_method,
|
|
203
|
+
index_exists=index_exists,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def run_pr_indexing(repo_path: Path) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Run the PR indexer for the given repository.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
repo_path: Path to the repository to index
|
|
213
|
+
"""
|
|
214
|
+
from cicada.pr_indexer.indexer import PRIndexer
|
|
215
|
+
from cicada.utils.storage import get_pr_index_path
|
|
216
|
+
|
|
217
|
+
print()
|
|
218
|
+
print(f"{BOLD}Indexing pull requests...{RESET}")
|
|
219
|
+
print()
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
indexer = PRIndexer(repo_path=str(repo_path))
|
|
223
|
+
output_path = get_pr_index_path(repo_path)
|
|
224
|
+
indexer.index_repository(output_path=str(output_path), incremental=True)
|
|
225
|
+
print()
|
|
226
|
+
print(f"{GREEN}✓{RESET} PR indexing complete!")
|
|
227
|
+
print()
|
|
228
|
+
except KeyboardInterrupt:
|
|
229
|
+
print()
|
|
230
|
+
print(f"{PRIMARY}⚠️ PR indexing interrupted by user.{RESET}")
|
|
231
|
+
print(
|
|
232
|
+
f"{GREY}Partial index may have been saved. Run 'cicada-pr-indexer' to continue.{RESET}"
|
|
233
|
+
)
|
|
234
|
+
print()
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print()
|
|
237
|
+
print(f"{PRIMARY}⚠️ PR indexing failed: {e}{RESET}")
|
|
238
|
+
print(f"{GREY}You can run 'cicada-pr-indexer' later to index PRs.{RESET}")
|
|
239
|
+
print()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def add_to_claude_md(repo_path: Path) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Add Cicada usage documentation to CLAUDE.md file.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
repo_path: Path to the repository
|
|
248
|
+
"""
|
|
249
|
+
print()
|
|
250
|
+
print(f"{BOLD}Adding Cicada guide to CLAUDE.md...{RESET}")
|
|
251
|
+
print()
|
|
252
|
+
|
|
253
|
+
claude_md_path = repo_path / "CLAUDE.md"
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
# Check if CLAUDE.md exists
|
|
257
|
+
if claude_md_path.exists():
|
|
258
|
+
# Read existing content
|
|
259
|
+
with open(claude_md_path) as f:
|
|
260
|
+
content = f.read()
|
|
261
|
+
|
|
262
|
+
# Check if cicada docs already exist
|
|
263
|
+
if "<cicada>" in content:
|
|
264
|
+
print(f"{GREY}Cicada documentation already exists in CLAUDE.md{RESET}")
|
|
265
|
+
print()
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Append to existing file
|
|
269
|
+
with open(claude_md_path, "a") as f:
|
|
270
|
+
f.write("\n" + CICADA_DOCS)
|
|
271
|
+
|
|
272
|
+
print(f"{GREEN}✓{RESET} Added Cicada guide to existing CLAUDE.md")
|
|
273
|
+
else:
|
|
274
|
+
# Create new CLAUDE.md file
|
|
275
|
+
with open(claude_md_path, "w") as f:
|
|
276
|
+
f.write("# Project Instructions for AI Assistants\n")
|
|
277
|
+
f.write(CICADA_DOCS)
|
|
278
|
+
|
|
279
|
+
print(f"{GREEN}✓{RESET} Created CLAUDE.md with Cicada guide")
|
|
280
|
+
|
|
281
|
+
print()
|
|
282
|
+
except Exception as e:
|
|
283
|
+
print()
|
|
284
|
+
print(f"{PRIMARY}⚠️ Failed to add Cicada guide to CLAUDE.md: {e}{RESET}")
|
|
285
|
+
print(f"{GREY}You can manually add the Cicada documentation later.{RESET}")
|
|
286
|
+
print()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def check_elixir_project(repo_path: Path) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Check if the given path is an Elixir project.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
repo_path: Path to check
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
NotElixirProjectError: If the path is not an Elixir project
|
|
298
|
+
"""
|
|
299
|
+
if not (repo_path / "mix.exs").exists():
|
|
300
|
+
raise NotElixirProjectError(
|
|
301
|
+
f"{repo_path} does not appear to be an Elixir project (mix.exs not found)"
|
|
302
|
+
)
|