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
@@ -2,7 +2,6 @@
2
2
 
3
3
  import sys
4
4
  from pathlib import Path
5
- from typing import cast
6
5
 
7
6
  try:
8
7
  from simple_term_menu import TerminalMenu
@@ -12,218 +11,271 @@ except ImportError:
12
11
  TerminalMenu = None # type: ignore
13
12
  has_terminal_menu = False
14
13
 
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
14
+ from cicada.format import BOLD, GREY, PRIMARY, RESET, SELECTED, generate_gradient_ascii_art
15
+ from cicada.interactive_setup_helpers import (
16
+ CLAUDE_MD_ITEMS,
17
+ EDITOR_ITEMS,
18
+ EDITOR_MAP_TEXT,
19
+ PR_ITEMS,
20
+ TIER_ITEMS,
21
+ TIER_MAP,
22
+ TIER_MAP_TEXT,
23
+ NotElixirProjectError,
24
+ add_to_claude_md,
25
+ check_elixir_project,
26
+ display_claude_md_selection,
27
+ display_editor_selection,
28
+ display_pr_indexing_selection,
29
+ display_tier_selection,
30
+ get_existing_config,
31
+ run_pr_indexing,
32
+ run_setup,
33
+ )
34
+
35
+ MENU_STYLE = {
36
+ "title": "",
37
+ "menu_cursor": "» ",
38
+ "menu_cursor_style": ("fg_yellow", "bold"),
39
+ "menu_highlight_style": ("fg_yellow", "bold"),
40
+ "cycle_cursor": True,
41
+ "clear_screen": False,
42
+ }
43
+
44
+
45
+ class MenuUnavailableError(Exception):
46
+ """Raised when TerminalMenu cannot be used for interactive prompts."""
47
+
48
+
49
+ def _print_first_time_intro(show_header: bool) -> None:
50
+ """Render the ASCII art banner and intro text."""
51
+ if show_header:
52
+ print(generate_gradient_ascii_art())
53
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
54
+ print(f"{SELECTED}🦗 Welcome to CICADA - Elixir Code Intelligence{RESET}")
55
+ print(f"{PRIMARY}{'=' * 70}{RESET}")
56
+ print()
57
+ print(f"This is your first time running CICADA in this project.{RESET}")
58
+ print(f"Let's configure keyword extraction for code intelligence.{RESET}")
59
+ print()
60
+
61
+
62
+ def _prompt_menu_selection(items: list[str], cancel_message: str) -> int:
63
+ """Display a menu and return the selected index."""
64
+ if TerminalMenu is None:
65
+ raise MenuUnavailableError
18
66
 
67
+ try:
68
+ menu = TerminalMenu(items, **MENU_STYLE) # type: ignore[arg-type]
69
+ except Exception:
70
+ raise MenuUnavailableError from None
19
71
 
20
- def _text_based_setup() -> tuple[str, str]:
72
+ try:
73
+ selection = menu.show()
74
+ except (KeyboardInterrupt, EOFError):
75
+ print()
76
+ print(cancel_message)
77
+ sys.exit(1)
78
+ except Exception:
79
+ raise MenuUnavailableError from None
80
+
81
+ if selection is None:
82
+ print()
83
+ print(cancel_message)
84
+ sys.exit(1)
85
+
86
+ if isinstance(selection, tuple):
87
+ selection = selection[0]
88
+
89
+ return int(selection)
90
+
91
+
92
+ def _handle_menu_unavailable() -> tuple[str, str, bool, bool]:
93
+ """Fallback to text-based setup when TerminalMenu cannot be used."""
94
+ print(
95
+ f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
96
+ file=sys.stderr,
97
+ )
98
+ return _text_based_setup()
99
+
100
+
101
+ def _text_based_setup() -> tuple[str, str, bool, bool]:
21
102
  """
22
103
  Fallback text-based setup for terminals that don't support simple-term-menu.
23
104
 
24
105
  Returns:
25
- tuple[str, str]: The selected extraction method and model tier
106
+ tuple[str, str, bool, bool]: The selected extraction method, expansion method,
107
+ whether to index PRs, and whether to add to CLAUDE.md
26
108
  """
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}")
109
+ _print_first_time_intro(show_header=True)
110
+ print(f"{BOLD}Step 1/3: Choose intelligence tier{RESET}")
35
111
  print()
36
- print("1. Lemminflect - Grammar-based keyword extraction (fast, proven)")
37
- print("2. KeyBERT - Semantic keyword extraction (AI embeddings)")
112
+ print("1. Fast - Term frequency + inflections (no downloads)")
113
+ print("2. Balanced - KeyBERT + GloVe semantic expansion (261MB)")
114
+ print("3. Maximum - KeyBERT + FastText expansion (1091MB)")
38
115
  print()
39
116
 
40
117
  while True:
41
118
  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"
119
+ tier_choice = input("Enter your choice (1, 2, or 3) [default: 1]: ").strip()
120
+ if not tier_choice:
121
+ tier_choice = "1"
122
+ if tier_choice in TIER_MAP_TEXT:
123
+ method, expansion_method = TIER_MAP_TEXT[tier_choice]
47
124
  break
48
- print("Invalid choice. Please enter 1 or 2.")
125
+ print("Invalid choice. Please enter 1, 2, or 3.")
49
126
  except (KeyboardInterrupt, EOFError):
50
127
  print()
51
128
  print("Setup cancelled. Exiting...")
52
129
  sys.exit(1)
53
130
 
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")
131
+ display_tier_selection(int(tier_choice) - 1)
63
132
 
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}")
133
+ # Step 2: Ask about PR indexing
134
+ print(f"{BOLD}Step 2/3: Index pull requests?{RESET}")
135
+ print(f"{PRIMARY} PR indexing enables fast offline lookup of GitHub PRs{RESET}")
136
+ print(f"{PRIMARY} Useful for: finding which PR introduced code, viewing PR context{RESET}")
67
137
  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}")
138
+ print("1. Yes - Index PRs now (requires GitHub access)")
139
+ print("2. No - Skip PR indexing (can run later with 'cicada-pr-indexer')")
74
140
  print()
75
141
 
76
142
  while True:
77
143
  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]
144
+ pr_choice = input("Enter your choice (1 or 2) [default: 2]: ").strip()
145
+ if not pr_choice:
146
+ pr_choice = "2"
147
+ if pr_choice in ("1", "2"):
148
+ index_prs = pr_choice == "1"
84
149
  break
85
- print("Invalid choice. Please enter 1, 2, or 3.")
150
+ print("Invalid choice. Please enter 1 or 2.")
86
151
  except (KeyboardInterrupt, EOFError):
87
152
  print()
88
153
  print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
89
154
  sys.exit(1)
90
155
 
156
+ display_pr_indexing_selection(index_prs)
157
+
158
+ # Step 3: Ask about adding to CLAUDE.md
159
+ print(f"{BOLD}Step 3/3: Augment CLAUDE.md for AI assistants?{RESET}")
160
+ print(f"{PRIMARY} Add documentation to CLAUDE.md to help AI assistants{RESET}")
161
+ print(f"{PRIMARY} understand when and how to use Cicada tools effectively{RESET}")
91
162
  print()
92
- print(f"{GREEN}✓{RESET} Selected: KeyBERT - {tier.capitalize()} model")
163
+ print("1. Yes - Add Cicada usage guide to CLAUDE.md (recommended)")
164
+ print("2. No - Skip CLAUDE.md setup")
93
165
  print()
94
166
 
95
- return ("bert", tier)
167
+ while True:
168
+ try:
169
+ claude_md_choice = input("Enter your choice (1 or 2) [default: 1]: ").strip()
170
+ if not claude_md_choice:
171
+ claude_md_choice = "1"
172
+ if claude_md_choice in ("1", "2"):
173
+ add_to_claude_md_flag = claude_md_choice == "1"
174
+ break
175
+ print("Invalid choice. Please enter 1 or 2.")
176
+ except (KeyboardInterrupt, EOFError):
177
+ print()
178
+ print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
179
+ sys.exit(1)
180
+
181
+ display_claude_md_selection(add_to_claude_md_flag)
182
+
183
+ return (method, expansion_method, index_prs, add_to_claude_md_flag)
96
184
 
97
185
 
98
- def show_first_time_setup() -> tuple[str, str]:
186
+ def show_first_time_setup(show_welcome: bool = True) -> tuple[str, str, bool, bool]:
99
187
  """
100
188
  Display an interactive first-time setup menu for cicada.
101
189
 
102
190
  Falls back to text-based input if the terminal doesn't support simple-term-menu.
103
191
 
192
+ Args:
193
+ show_welcome: Whether to display the ASCII art banner and intro text.
194
+
104
195
  Returns:
105
- tuple[str, str]: The selected extraction method and model tier
106
- e.g., ('lemminflect', 'regular') or ('bert', 'fast')
196
+ tuple[str, str, bool, bool]: The selected extraction method, expansion method,
197
+ whether to index PRs, and whether to add to CLAUDE.md
198
+ e.g., ('regular', 'lemmi', False, True) or ('bert', 'glove', True, True)
107
199
  """
108
200
  # Check if terminal menu is available and supported
109
201
  if not has_terminal_menu:
110
202
  return _text_based_setup()
111
203
 
112
- # Display ASCII art
113
- print(generate_gradient_ascii_art())
204
+ _print_first_time_intro(show_header=show_welcome)
205
+ print(f"{BOLD}Step 1/3: Choose intelligence tier{RESET}")
114
206
 
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}")
207
+ def _select_with_menu(items: list[str], cancel_message: str) -> int | None:
208
+ try:
209
+ return _prompt_menu_selection(items, cancel_message)
210
+ except MenuUnavailableError:
211
+ return None
124
212
 
125
- method_items = [
126
- "Lemminflect - Grammar-based keyword extraction (fast, proven)",
127
- "KeyBERT - Semantic keyword extraction (AI embeddings)",
128
- ]
213
+ tier_index = _select_with_menu(TIER_ITEMS, "Setup cancelled. Exiting...")
214
+ if tier_index is None:
215
+ return _handle_menu_unavailable()
129
216
 
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()
217
+ method, expansion_method = TIER_MAP[tier_index]
218
+ display_tier_selection(tier_index)
154
219
 
155
- if method_index is None:
156
- print()
157
- print("Setup cancelled. Exiting...")
158
- sys.exit(1)
220
+ # Step 2: Ask about PR indexing
221
+ print(f"{BOLD}Step 2/3: Index pull requests?{RESET}")
222
+ print(f"{PRIMARY} PR indexing enables fast offline lookup of GitHub PRs{RESET}")
223
+ print(f"{PRIMARY} Useful for: finding which PR introduced code, viewing PR context{RESET}")
224
+ print()
159
225
 
160
- method = "lemminflect" if method_index == 0 else "bert"
226
+ pr_index = _select_with_menu(
227
+ PR_ITEMS,
228
+ f"{SELECTED}Setup cancelled. Exiting...{RESET}",
229
+ )
230
+ if pr_index is None:
231
+ return _handle_menu_unavailable()
161
232
 
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")
233
+ index_prs = pr_index == 1
234
+ display_pr_indexing_selection(index_prs)
173
235
 
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}')
236
+ # Step 3: Ask about adding to CLAUDE.md
237
+ print(f"{BOLD}Step 3/3: Augment CLAUDE.md for AI assistants?{RESET}")
238
+ print(f"{PRIMARY} Add documentation to CLAUDE.md to help AI assistants{RESET}")
239
+ print(f"{PRIMARY} understand when and how to use Cicada tools effectively{RESET}")
179
240
  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
241
 
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()
242
+ claude_md_index = _select_with_menu(
243
+ CLAUDE_MD_ITEMS,
244
+ f"{SELECTED}Setup cancelled. Exiting...{RESET}",
245
+ )
246
+ if claude_md_index is None:
247
+ return _handle_menu_unavailable()
212
248
 
213
- if tier_index is None:
214
- print()
215
- print(f"{SELECTED}Setup cancelled. Exiting...{RESET}")
216
- sys.exit(1)
249
+ add_to_claude_md_flag = claude_md_index == 0 # "Yes" is at index 0
250
+ display_claude_md_selection(add_to_claude_md_flag)
217
251
 
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]]
252
+ return (method, expansion_method, index_prs, add_to_claude_md_flag)
221
253
 
222
- print()
223
- print(f"{GREEN}✓{RESET} Selected: KeyBERT - {tier.capitalize()} model")
254
+
255
+ def _text_based_editor_selection() -> str:
256
+ """
257
+ Fallback text-based editor selection for terminals that don't support simple-term-menu.
258
+
259
+ Returns:
260
+ str: The selected editor ('claude', 'cursor', or 'vs')
261
+ """
262
+ print("1. Claude Code - AI-powered code editor")
263
+ print("2. Cursor - AI-first code editor")
264
+ print("3. VS Code - Visual Studio Code")
224
265
  print()
225
266
 
226
- return ("bert", tier)
267
+ while True:
268
+ try:
269
+ choice = input("Enter your choice (1, 2, or 3) [default: 1]: ").strip()
270
+ if not choice:
271
+ choice = "1"
272
+ if choice in EDITOR_MAP_TEXT:
273
+ return EDITOR_MAP_TEXT[choice]
274
+ print("Invalid choice. Please enter 1, 2, or 3.")
275
+ except (KeyboardInterrupt, EOFError):
276
+ print()
277
+ print("Setup cancelled. Exiting...")
278
+ sys.exit(1)
227
279
 
228
280
 
229
281
  def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
@@ -235,13 +287,27 @@ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
235
287
  Args:
236
288
  repo_path: Path to the Elixir repository. Defaults to current directory.
237
289
  """
238
- from cicada.setup import setup
290
+
291
+ # Helper to run setup with error handling
292
+ def _run_setup_with_error_handling(
293
+ editor: str,
294
+ repo_path: Path,
295
+ extraction_method: str,
296
+ expansion_method: str,
297
+ index_exists: bool = False,
298
+ ) -> None:
299
+ try:
300
+ run_setup(editor, repo_path, extraction_method, expansion_method, index_exists)
301
+ except Exception as e:
302
+ print(f"\n{PRIMARY}Error: Setup failed: {e}{RESET}")
303
+ sys.exit(1)
239
304
 
240
305
  # Check if we're in an Elixir project
241
306
  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}")
307
+ try:
308
+ check_elixir_project(repo_path)
309
+ except NotElixirProjectError as e:
310
+ print(f"{PRIMARY}Error: {e}{RESET}")
245
311
  print()
246
312
  print("Please run cicada from the root of an Elixir project.")
247
313
  sys.exit(1)
@@ -256,22 +322,15 @@ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
256
322
  print()
257
323
  print(f"Let's set up Cicada for your editor and project.{RESET}")
258
324
  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
- ]
325
+ print(f"{BOLD}Step 1/4: Choose your editor{RESET}")
266
326
 
267
327
  if has_terminal_menu:
268
328
  try:
269
329
  if TerminalMenu is None:
270
- # Fallback to text-based
271
330
  editor = _text_based_editor_selection()
272
331
  else:
273
332
  editor_menu = TerminalMenu(
274
- editor_items,
333
+ EDITOR_ITEMS,
275
334
  title="",
276
335
  menu_cursor="» ",
277
336
  menu_cursor_style=("fg_yellow", "bold"),
@@ -286,7 +345,7 @@ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
286
345
  print("Setup cancelled. Exiting...")
287
346
  sys.exit(1)
288
347
 
289
- editor_map = {0: "claude", 1: "cursor", 2: "vs"}
348
+ editor_map = {0: "claude", 1: "cursor", 2: "vs", 3: "gemini", 4: "codex"}
290
349
  editor = editor_map[
291
350
  int(editor_index) if isinstance(editor_index, int) else editor_index[0]
292
351
  ]
@@ -295,7 +354,6 @@ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
295
354
  print("Setup cancelled. Exiting...")
296
355
  sys.exit(1)
297
356
  except Exception:
298
- # Terminal doesn't support the menu - fall back to text-based
299
357
  print(
300
358
  f"\n{GREY}Note: Terminal menu not supported, using text-based input{RESET}\n",
301
359
  file=sys.stderr,
@@ -304,163 +362,31 @@ def show_full_interactive_setup(repo_path: str | Path | None = None) -> None:
304
362
  else:
305
363
  editor = _text_based_editor_selection()
306
364
 
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}")
365
+ display_editor_selection(editor)
347
366
 
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}"
367
+ # Check if index already exists
368
+ existing_config = get_existing_config(repo_path)
369
+ if existing_config is not None:
370
+ extraction_method, expansion_method = existing_config
371
+ _run_setup_with_error_handling(
372
+ editor, repo_path, extraction_method, expansion_method, index_exists=True
405
373
  )
374
+ return
406
375
 
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()
376
+ extraction_method, expansion_method, index_prs, add_to_claude_md_flag = show_first_time_setup(
377
+ show_welcome=False
378
+ )
454
379
 
455
- # Run setup
456
380
  print(f"{BOLD}Running setup...{RESET}")
457
381
  print()
458
382
 
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)
383
+ _run_setup_with_error_handling(editor, repo_path, extraction_method, expansion_method)
384
+
385
+ if index_prs:
386
+ run_pr_indexing(repo_path)
387
+
388
+ if add_to_claude_md_flag:
389
+ add_to_claude_md(repo_path)
464
390
 
465
391
 
466
392
  def _text_based_editor_selection() -> str:
@@ -468,22 +394,24 @@ def _text_based_editor_selection() -> str:
468
394
  Fallback text-based editor selection for terminals that don't support simple-term-menu.
469
395
 
470
396
  Returns:
471
- str: The selected editor ('claude', 'cursor', or 'vs')
397
+ str: The selected editor ('claude', 'cursor', 'vs', 'gemini', or 'codex')
472
398
  """
473
399
  print("1. Claude Code - AI-powered code editor")
474
400
  print("2. Cursor - AI-first code editor")
475
401
  print("3. VS Code - Visual Studio Code")
402
+ print("4. Gemini CLI - Google Gemini command line interface")
403
+ print("5. Codex - AI code editor")
476
404
  print()
477
405
 
478
406
  while True:
479
407
  try:
480
- choice = input("Enter your choice (1, 2, or 3) [default: 1]: ").strip()
408
+ choice = input("Enter your choice (1-5) [default: 1]: ").strip()
481
409
  if not choice:
482
410
  choice = "1"
483
- if choice in ("1", "2", "3"):
484
- editor_map = {"1": "claude", "2": "cursor", "3": "vs"}
411
+ if choice in ("1", "2", "3", "4", "5"):
412
+ editor_map = {"1": "claude", "2": "cursor", "3": "vs", "4": "gemini", "5": "codex"}
485
413
  return editor_map[choice]
486
- print("Invalid choice. Please enter 1, 2, or 3.")
414
+ print("Invalid choice. Please enter 1-5.")
487
415
  except (KeyboardInterrupt, EOFError):
488
416
  print()
489
417
  print("Setup cancelled. Exiting...")