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
@@ -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
+ )