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
cicada/interactive_setup.py
CHANGED
|
@@ -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.
|
|
16
|
-
from cicada.
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
print(f"{
|
|
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.
|
|
37
|
-
print("2.
|
|
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
|
-
|
|
43
|
-
if not
|
|
44
|
-
|
|
45
|
-
if
|
|
46
|
-
method =
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
65
|
-
print(f"{
|
|
66
|
-
print(f"{PRIMARY}
|
|
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.
|
|
69
|
-
print("2.
|
|
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
|
-
|
|
79
|
-
if not
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
print(
|
|
204
|
+
_print_first_time_intro(show_header=show_welcome)
|
|
205
|
+
print(f"{BOLD}Step 1/3: Choose intelligence tier{RESET}")
|
|
114
206
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
#
|
|
175
|
-
print(f"{
|
|
176
|
-
print(f"{PRIMARY}
|
|
177
|
-
print(f
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 '
|
|
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
|
|
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
|
|
414
|
+
print("Invalid choice. Please enter 1-5.")
|
|
487
415
|
except (KeyboardInterrupt, EOFError):
|
|
488
416
|
print()
|
|
489
417
|
print("Setup cancelled. Exiting...")
|