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
@@ -0,0 +1,490 @@
1
+ """Interactive first-time setup menu for cicada."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ try:
8
+ from simple_term_menu import TerminalMenu
9
+
10
+ has_terminal_menu = True
11
+ except ImportError:
12
+ TerminalMenu = None # type: ignore
13
+ has_terminal_menu = False
14
+
15
+ from cicada.ascii_art import generate_gradient_ascii_art
16
+ from cicada.colors import BOLD, GREEN, GREY, PRIMARY, RESET, SELECTED
17
+ from cicada.setup import EditorType
18
+
19
+
20
+ def _text_based_setup() -> tuple[str, str]:
21
+ """
22
+ Fallback text-based setup for terminals that don't support simple-term-menu.
23
+
24
+ Returns:
25
+ tuple[str, str]: The selected extraction method and model tier
26
+ """
27
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
28
+ print(f"{SELECTED}🦗 Welcome to CICADA - Elixir Code Intelligence{RESET}")
29
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
30
+ print()
31
+ print(f"This is your first time running CICADA in this project.{RESET}")
32
+ print(f"Let's configure keyword extraction for code intelligence.{RESET}")
33
+ print()
34
+ print(f"{BOLD}Step 1/2: Choose extraction method{RESET}")
35
+ print()
36
+ print("1. Lemminflect - Grammar-based keyword extraction (fast, proven)")
37
+ print("2. KeyBERT - Semantic keyword extraction (AI embeddings)")
38
+ print()
39
+
40
+ while True:
41
+ try:
42
+ method_choice = input("Enter your choice (1 or 2) [default: 1]: ").strip()
43
+ if not method_choice:
44
+ method_choice = "1"
45
+ if method_choice in ("1", "2"):
46
+ method = "lemminflect" if method_choice == "1" else "bert"
47
+ break
48
+ print("Invalid choice. Please enter 1 or 2.")
49
+ except (KeyboardInterrupt, EOFError):
50
+ print()
51
+ print("Setup cancelled. Exiting...")
52
+ sys.exit(1)
53
+
54
+ # For lemminflect, no tier selection - it's always the same
55
+ print()
56
+ if method == "lemminflect":
57
+ print(f"{BOLD} What is Lemminflect?{RESET}")
58
+ print(f" Lemminflect finds keywords using grammar rules + word importance{RESET}")
59
+ print()
60
+ print(f"{GREEN}✓{RESET} Selected: LEMMINFLECT")
61
+ print()
62
+ return ("lemminflect", "regular")
63
+
64
+ # For KeyBERT, ask for tier
65
+ print(f"{SELECTED} What is KeyBERT?{RESET}")
66
+ print(f"{PRIMARY} KeyBERT uses AI embeddings to find semantically similar keywords{RESET}")
67
+ print()
68
+ print("1. Fast (80MB, ~1s) - Recommended for bigger projects")
69
+ print("2. Regular (133MB, ~1.4s) - Better semantic understanding [recommended]")
70
+ print("3. Max (420MB, ~6.5s) - Highest quality embeddings")
71
+
72
+ print()
73
+ print(f"{BOLD}Step 2/2: Choose model tier{RESET}")
74
+ print()
75
+
76
+ while True:
77
+ try:
78
+ tier_choice = input("Enter your choice (1, 2, or 3) [default: 2]: ").strip()
79
+ if not tier_choice:
80
+ tier_choice = "2"
81
+ if tier_choice in ("1", "2", "3"):
82
+ tier_map = {"1": "fast", "2": "regular", "3": "max"}
83
+ tier = tier_map[tier_choice]
84
+ break
85
+ print("Invalid choice. Please enter 1, 2, or 3.")
86
+ except (KeyboardInterrupt, EOFError):
87
+ print()
88
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
89
+ sys.exit(1)
90
+
91
+ print()
92
+ print(f"{GREEN}✓{RESET} Selected: KeyBERT - {tier.capitalize()} model")
93
+ print()
94
+
95
+ return ("bert", tier)
96
+
97
+
98
+ def show_first_time_setup() -> tuple[str, str]:
99
+ """
100
+ Display an interactive first-time setup menu for cicada.
101
+
102
+ Falls back to text-based input if the terminal doesn't support simple-term-menu.
103
+
104
+ Returns:
105
+ tuple[str, str]: The selected extraction method and model tier
106
+ e.g., ('lemminflect', 'regular') or ('bert', 'fast')
107
+ """
108
+ # Check if terminal menu is available and supported
109
+ if not has_terminal_menu:
110
+ return _text_based_setup()
111
+
112
+ # Display ASCII art
113
+ print(generate_gradient_ascii_art())
114
+
115
+ # Step 1: Choose extraction method
116
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
117
+ print(f"{SELECTED}🦗 Welcome to CICADA - Elixir Code Intelligence{RESET}")
118
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
119
+ print()
120
+ print(f"This is your first time running CICADA in this project.{RESET}")
121
+ print(f"Let's configure keyword extraction for code intelligence.{RESET}")
122
+ print()
123
+ print(f"{BOLD}Step 1/2: Choose extraction method{RESET}")
124
+
125
+ method_items = [
126
+ "Lemminflect - Grammar-based keyword extraction (fast, proven)",
127
+ "KeyBERT - Semantic keyword extraction (AI embeddings)",
128
+ ]
129
+
130
+ try:
131
+ if TerminalMenu is None:
132
+ return _text_based_setup()
133
+ method_menu = TerminalMenu(
134
+ method_items,
135
+ title="",
136
+ menu_cursor="» ",
137
+ menu_cursor_style=("fg_yellow", "bold"),
138
+ menu_highlight_style=("fg_yellow", "bold"),
139
+ cycle_cursor=True,
140
+ clear_screen=False,
141
+ )
142
+ method_index = method_menu.show()
143
+ except (KeyboardInterrupt, EOFError):
144
+ print()
145
+ print("Setup cancelled. Exiting...")
146
+ sys.exit(1)
147
+ except Exception:
148
+ # Terminal doesn't support the menu - fall back to text-based
149
+ print(
150
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
151
+ file=sys.stderr,
152
+ )
153
+ return _text_based_setup()
154
+
155
+ if method_index is None:
156
+ print()
157
+ print("Setup cancelled. Exiting...")
158
+ sys.exit(1)
159
+
160
+ method = "lemminflect" if method_index == 0 else "bert"
161
+
162
+ # For lemminflect, no tier selection - it's always the same
163
+ print()
164
+ if method == "lemminflect":
165
+ print(f"{BOLD} What is Lemminflect?{RESET}")
166
+ print(f" Lemminflect finds keywords using grammar rules + word importance{RESET}")
167
+ print(f' Example: "We use Kubernetes for container orchestration"{RESET}')
168
+ print(f' Output: "Kubernetes", "container", "orchestration"{RESET}')
169
+ print()
170
+ print(f"{GREEN}✓{RESET} Selected: LEMMINFLECT")
171
+ print()
172
+ return ("lemminflect", "regular")
173
+
174
+ # For KeyBERT, ask for tier
175
+ print(f"{SELECTED} What is KeyBERT?{RESET}")
176
+ print(f"{PRIMARY} KeyBERT uses AI embeddings to find semantically similar keywords{RESET}")
177
+ print(f'{PRIMARY} Example: "We use Kubernetes for container orchestration"{RESET}')
178
+ print(f'{PRIMARY} Output: "Kubernetes", "deployment", "microservices", "DevOps"{RESET}')
179
+ print()
180
+ tier_items = [
181
+ "Fast (80MB, ~1s) - Recommended for bigger projects",
182
+ "Regular [recommended] (133MB, ~1.4s) - Better semantic understanding",
183
+ "Max (420MB, ~6.5s) - Highest quality embeddings",
184
+ ]
185
+ print(f"{SELECTED}Step 2/2: Choose model tier\n")
186
+
187
+ try:
188
+ if TerminalMenu is None:
189
+ return _text_based_setup()
190
+ tier_menu = TerminalMenu(
191
+ tier_items,
192
+ title="",
193
+ menu_cursor="» ",
194
+ menu_cursor_style=("fg_yellow", "bold"),
195
+ menu_highlight_style=("fg_yellow", "bold"),
196
+ cycle_cursor=True,
197
+ clear_screen=False,
198
+ )
199
+ tier_index = tier_menu.show()
200
+ except (KeyboardInterrupt, EOFError):
201
+ print()
202
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
203
+ sys.exit(1)
204
+ except Exception:
205
+ # Terminal doesn't support the menu - fall back to text-based
206
+ print(
207
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
208
+ file=sys.stderr,
209
+ )
210
+ # Recreate the selection for model tier based on already selected method
211
+ return _text_based_setup()
212
+
213
+ if tier_index is None:
214
+ print()
215
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
216
+ sys.exit(1)
217
+
218
+ tier_map = {0: "fast", 1: "regular", 2: "max"}
219
+ # Ensure tier_index is treated as int (TerminalMenu.show() returns int | tuple | None)
220
+ tier = tier_map[int(tier_index) if isinstance(tier_index, int) else tier_index[0]]
221
+
222
+ print()
223
+ print(f"{GREEN}✓{RESET} Selected: KeyBERT - {tier.capitalize()} model")
224
+ print()
225
+
226
+ return ("bert", tier)
227
+
228
+
229
+ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
230
+ """
231
+ Display full interactive setup including editor selection and keyword extraction.
232
+
233
+ This is the main entry point when running `cicada` with no arguments or a path.
234
+
235
+ Args:
236
+ repo_path: Path to the Elixir repository. Defaults to current directory.
237
+ """
238
+ from cicada.setup import setup
239
+
240
+ # Check if we're in an Elixir project
241
+ repo_path = Path.cwd() if repo_path is None else Path(repo_path).resolve()
242
+ if not (repo_path / "mix.exs").exists():
243
+ print(f"{PRIMARY}Error: {repo_path} does not appear to be an Elixir project{RESET}")
244
+ print(f"{GREY}(mix.exs not found){RESET}")
245
+ print()
246
+ print("Please run cicada from the root of an Elixir project.")
247
+ sys.exit(1)
248
+
249
+ # Display ASCII art
250
+ print(generate_gradient_ascii_art())
251
+
252
+ # Step 1: Choose editor
253
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
254
+ print(f"{SELECTED}🦗 Welcome to CICADA - Elixir Code Intelligence{RESET}")
255
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
256
+ print()
257
+ print(f"Let's set up Cicada for your editor and project.{RESET}")
258
+ print()
259
+ print(f"{BOLD}Step 1/3: Choose your editor{RESET}")
260
+
261
+ editor_items = [
262
+ "Claude Code - AI-powered code editor",
263
+ "Cursor - AI-first code editor",
264
+ "VS Code - Visual Studio Code",
265
+ ]
266
+
267
+ if has_terminal_menu:
268
+ try:
269
+ if TerminalMenu is None:
270
+ # Fallback to text-based
271
+ editor = _text_based_editor_selection()
272
+ else:
273
+ editor_menu = TerminalMenu(
274
+ editor_items,
275
+ title="",
276
+ menu_cursor="» ",
277
+ menu_cursor_style=("fg_yellow", "bold"),
278
+ menu_highlight_style=("fg_yellow", "bold"),
279
+ cycle_cursor=True,
280
+ clear_screen=False,
281
+ )
282
+ editor_index = editor_menu.show()
283
+
284
+ if editor_index is None:
285
+ print()
286
+ print("Setup cancelled. Exiting...")
287
+ sys.exit(1)
288
+
289
+ editor_map = {0: "claude", 1: "cursor", 2: "vs"}
290
+ editor = editor_map[
291
+ int(editor_index) if isinstance(editor_index, int) else editor_index[0]
292
+ ]
293
+ except (KeyboardInterrupt, EOFError):
294
+ print()
295
+ print("Setup cancelled. Exiting...")
296
+ sys.exit(1)
297
+ except Exception:
298
+ # Terminal doesn't support the menu - fall back to text-based
299
+ print(
300
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
301
+ file=sys.stderr,
302
+ )
303
+ editor = _text_based_editor_selection()
304
+ else:
305
+ editor = _text_based_editor_selection()
306
+
307
+ print()
308
+ print(f"{GREEN}✓{RESET} Selected: {editor.upper()}")
309
+ print()
310
+
311
+ # Check if index already exists before showing model selection
312
+ from cicada.utils.storage import get_config_path, get_index_path
313
+
314
+ config_path = get_config_path(repo_path)
315
+ index_path = get_index_path(repo_path)
316
+
317
+ if config_path.exists() and index_path.exists():
318
+ # Index exists - use existing settings, don't show model selection
319
+ import yaml
320
+
321
+ try:
322
+ with open(config_path) as f:
323
+ existing_config = yaml.safe_load(f)
324
+ method = existing_config.get("keyword_extraction", {}).get("method", "lemminflect")
325
+ tier = existing_config.get("keyword_extraction", {}).get("tier", "regular")
326
+
327
+ # Run setup with existing settings
328
+ try:
329
+ setup(
330
+ cast(EditorType, editor),
331
+ repo_path,
332
+ keyword_method=method,
333
+ keyword_tier=tier,
334
+ index_exists=True,
335
+ )
336
+ except Exception as e:
337
+ print(f"\n{PRIMARY}Error: Setup failed: {e}{RESET}")
338
+ sys.exit(1)
339
+
340
+ return # Exit early - don't show model selection
341
+ except Exception:
342
+ # If we can't read config, proceed with model selection
343
+ pass
344
+
345
+ # Step 2: Choose keyword extraction method
346
+ print(f"{BOLD}Step 2/3: Choose extraction method{RESET}")
347
+
348
+ method_items = [
349
+ "Lemminflect - Grammar-based keyword extraction (fast, proven)",
350
+ "KeyBERT - Semantic keyword extraction (AI embeddings)",
351
+ ]
352
+
353
+ if has_terminal_menu:
354
+ try:
355
+ if TerminalMenu is None:
356
+ method, tier = show_first_time_setup()
357
+ return
358
+ method_menu = TerminalMenu(
359
+ method_items,
360
+ title="",
361
+ menu_cursor="» ",
362
+ menu_cursor_style=("fg_yellow", "bold"),
363
+ menu_highlight_style=("fg_yellow", "bold"),
364
+ cycle_cursor=True,
365
+ clear_screen=False,
366
+ )
367
+ method_index = method_menu.show()
368
+
369
+ if method_index is None:
370
+ print()
371
+ print("Setup cancelled. Exiting...")
372
+ sys.exit(1)
373
+
374
+ method = "lemminflect" if method_index == 0 else "bert"
375
+ except (KeyboardInterrupt, EOFError):
376
+ print()
377
+ print("Setup cancelled. Exiting...")
378
+ sys.exit(1)
379
+ except Exception:
380
+ print(
381
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
382
+ file=sys.stderr,
383
+ )
384
+ method, tier = show_first_time_setup()
385
+ return
386
+ else:
387
+ method, tier = show_first_time_setup()
388
+ return
389
+
390
+ # For lemminflect, no tier selection needed - always uses default
391
+ if method == "lemminflect":
392
+ print()
393
+ print(f"{BOLD} What is Lemminflect?{RESET}")
394
+ print(f" Lemminflect finds keywords using grammar rules + word importance{RESET}")
395
+ print()
396
+ print(f"{GREEN}✓{RESET} Selected: LEMMINFLECT")
397
+ print()
398
+ tier = "regular" # Default tier (not used for lemminflect, but needed for API)
399
+ else:
400
+ # Step 3: Choose model tier (only for BERT)
401
+ print()
402
+ print(f"{SELECTED} What is KeyBERT?{RESET}")
403
+ print(
404
+ f"{PRIMARY} KeyBERT uses AI embeddings to find semantically similar keywords{RESET}"
405
+ )
406
+
407
+ tier_items = [
408
+ "Fast (80MB, ~1s) - Recommended for bigger projects",
409
+ "Regular [recommended] (133MB, ~1.4s) - Better semantic understanding",
410
+ "Max (420MB, ~6.5s) - Highest quality embeddings",
411
+ ]
412
+
413
+ print()
414
+ print(f"{BOLD}Step 3/3: Choose model tier{RESET}")
415
+ print()
416
+
417
+ try:
418
+ if TerminalMenu is None:
419
+ method, tier = show_first_time_setup()
420
+ return
421
+ tier_menu = TerminalMenu(
422
+ tier_items,
423
+ title="",
424
+ menu_cursor="» ",
425
+ menu_cursor_style=("fg_yellow", "bold"),
426
+ menu_highlight_style=("fg_yellow", "bold"),
427
+ cycle_cursor=True,
428
+ clear_screen=False,
429
+ )
430
+ tier_index = tier_menu.show()
431
+ except (KeyboardInterrupt, EOFError):
432
+ print()
433
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
434
+ sys.exit(1)
435
+ except Exception:
436
+ print(
437
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
438
+ file=sys.stderr,
439
+ )
440
+ method, tier = show_first_time_setup()
441
+ return
442
+
443
+ if tier_index is None:
444
+ print()
445
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
446
+ sys.exit(1)
447
+
448
+ tier_map = {0: "fast", 1: "regular", 2: "max"}
449
+ tier = tier_map[int(tier_index) if isinstance(tier_index, int) else tier_index[0]]
450
+
451
+ print()
452
+ print(f"{GREEN}✓{RESET} Selected: KeyBERT - {tier.capitalize()} model")
453
+ print()
454
+
455
+ # Run setup
456
+ print(f"{BOLD}Running setup...{RESET}")
457
+ print()
458
+
459
+ try:
460
+ setup(cast(EditorType, editor), repo_path, keyword_method=method, keyword_tier=tier)
461
+ except Exception as e:
462
+ print(f"\n{PRIMARY}Error: Setup failed: {e}{RESET}")
463
+ sys.exit(1)
464
+
465
+
466
+ def _text_based_editor_selection() -> str:
467
+ """
468
+ Fallback text-based editor selection for terminals that don't support simple-term-menu.
469
+
470
+ Returns:
471
+ str: The selected editor ('claude', 'cursor', or 'vs')
472
+ """
473
+ print("1. Claude Code - AI-powered code editor")
474
+ print("2. Cursor - AI-first code editor")
475
+ print("3. VS Code - Visual Studio Code")
476
+ print()
477
+
478
+ while True:
479
+ try:
480
+ choice = input("Enter your choice (1, 2, or 3) [default: 1]: ").strip()
481
+ if not choice:
482
+ choice = "1"
483
+ if choice in ("1", "2", "3"):
484
+ editor_map = {"1": "claude", "2": "cursor", "3": "vs"}
485
+ return editor_map[choice]
486
+ print("Invalid choice. Please enter 1, 2, or 3.")
487
+ except (KeyboardInterrupt, EOFError):
488
+ print()
489
+ print("Setup cancelled. Exiting...")
490
+ sys.exit(1)