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/commands.py
ADDED
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Command Handlers - Centralizes argparse logic and all CLI command handlers.
|
|
3
|
+
|
|
4
|
+
This module defines the argument parser and individual handler functions for all
|
|
5
|
+
Cicada CLI commands. It aims to consolidate command-line interface logic,
|
|
6
|
+
making `cli.py` a thin entry point and `mcp_entry.py` focused solely on MCP server startup.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Import tier resolution functions from centralized module
|
|
14
|
+
from cicada.tier import (
|
|
15
|
+
determine_tier,
|
|
16
|
+
get_extraction_expansion_methods,
|
|
17
|
+
tier_flag_specified,
|
|
18
|
+
validate_tier_flags,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Default debounce interval for watch mode (in seconds)
|
|
22
|
+
DEFAULT_WATCH_DEBOUNCE = 2.0
|
|
23
|
+
|
|
24
|
+
KNOWN_SUBCOMMANDS: tuple[str, ...] = (
|
|
25
|
+
"install",
|
|
26
|
+
"server",
|
|
27
|
+
"claude",
|
|
28
|
+
"cursor",
|
|
29
|
+
"vs",
|
|
30
|
+
"gemini",
|
|
31
|
+
"codex",
|
|
32
|
+
"watch",
|
|
33
|
+
"index",
|
|
34
|
+
"index-pr",
|
|
35
|
+
"find-dead-code",
|
|
36
|
+
"clean",
|
|
37
|
+
"dir",
|
|
38
|
+
)
|
|
39
|
+
KNOWN_SUBCOMMANDS_SET = frozenset(KNOWN_SUBCOMMANDS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _setup_and_start_watcher(args, repo_path_str: str) -> None:
|
|
43
|
+
"""Shared logic for starting file watcher.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
args: Parsed command-line arguments
|
|
47
|
+
repo_path_str: Path to the repository as a string
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
SystemExit: If configuration is invalid or watcher fails to start
|
|
51
|
+
"""
|
|
52
|
+
from cicada.utils.storage import get_config_path
|
|
53
|
+
from cicada.watcher import FileWatcher
|
|
54
|
+
|
|
55
|
+
# Validate tier flags
|
|
56
|
+
validate_tier_flags(args, require_force=True)
|
|
57
|
+
|
|
58
|
+
# Resolve repository path
|
|
59
|
+
repo_path = Path(repo_path_str).resolve()
|
|
60
|
+
config_path = get_config_path(repo_path)
|
|
61
|
+
|
|
62
|
+
# Determine tier using helper
|
|
63
|
+
tier = determine_tier(args, repo_path)
|
|
64
|
+
|
|
65
|
+
# Check if config exists when no tier is specified
|
|
66
|
+
tier_specified = tier_flag_specified(args)
|
|
67
|
+
if not tier_specified and not config_path.exists():
|
|
68
|
+
_print_tier_requirement_error()
|
|
69
|
+
print("\nRun 'cicada watch --help' for more information.", file=sys.stderr)
|
|
70
|
+
sys.exit(2)
|
|
71
|
+
|
|
72
|
+
# Create and start watcher
|
|
73
|
+
try:
|
|
74
|
+
watcher = FileWatcher(
|
|
75
|
+
repo_path=str(repo_path),
|
|
76
|
+
debounce_seconds=getattr(args, "debounce", DEFAULT_WATCH_DEBOUNCE),
|
|
77
|
+
verbose=True,
|
|
78
|
+
tier=tier,
|
|
79
|
+
)
|
|
80
|
+
watcher.start_watching()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
print("\nWatch mode stopped by user.")
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_argument_parser():
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
prog="cicada",
|
|
92
|
+
description="Cicada - AI-powered Elixir code analysis and search",
|
|
93
|
+
epilog="Run 'cicada <command> --help' for more information on a command.",
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"-v",
|
|
97
|
+
"--version",
|
|
98
|
+
action="version",
|
|
99
|
+
version="%(prog)s version from subcommand",
|
|
100
|
+
help="Show version and commit hash",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
104
|
+
|
|
105
|
+
install_parser = subparsers.add_parser(
|
|
106
|
+
"install",
|
|
107
|
+
help="Interactive setup for Cicada",
|
|
108
|
+
description="Interactive setup with editor and model selection",
|
|
109
|
+
)
|
|
110
|
+
install_parser.add_argument(
|
|
111
|
+
"repo",
|
|
112
|
+
nargs="?",
|
|
113
|
+
default=None,
|
|
114
|
+
help="Path to Elixir repository (default: current directory)",
|
|
115
|
+
)
|
|
116
|
+
install_parser.add_argument(
|
|
117
|
+
"--claude",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help="Skip editor selection, use Claude Code",
|
|
120
|
+
)
|
|
121
|
+
install_parser.add_argument(
|
|
122
|
+
"--cursor",
|
|
123
|
+
action="store_true",
|
|
124
|
+
help="Skip editor selection, use Cursor",
|
|
125
|
+
)
|
|
126
|
+
install_parser.add_argument(
|
|
127
|
+
"--vs",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Skip editor selection, use VS Code",
|
|
130
|
+
)
|
|
131
|
+
install_parser.add_argument(
|
|
132
|
+
"--gemini",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="Skip editor selection, use Gemini CLI",
|
|
135
|
+
)
|
|
136
|
+
install_parser.add_argument(
|
|
137
|
+
"--codex",
|
|
138
|
+
action="store_true",
|
|
139
|
+
help="Skip editor selection, use Codex",
|
|
140
|
+
)
|
|
141
|
+
install_parser.add_argument(
|
|
142
|
+
"--fast",
|
|
143
|
+
action="store_true",
|
|
144
|
+
help="Fast tier: Regular extraction + lemmi expansion (no downloads)",
|
|
145
|
+
)
|
|
146
|
+
install_parser.add_argument(
|
|
147
|
+
"--regular",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Regular tier: KeyBERT small + GloVe expansion (128MB, default)",
|
|
150
|
+
)
|
|
151
|
+
install_parser.add_argument(
|
|
152
|
+
"--max",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="Max tier: KeyBERT large + FastText expansion (958MB+)",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
server_parser = subparsers.add_parser(
|
|
158
|
+
"server",
|
|
159
|
+
help="Start MCP server (silent mode with defaults)",
|
|
160
|
+
description="Start MCP server with auto-setup using defaults",
|
|
161
|
+
)
|
|
162
|
+
server_parser.add_argument(
|
|
163
|
+
"repo",
|
|
164
|
+
nargs="?",
|
|
165
|
+
default=None,
|
|
166
|
+
help="Path to Elixir repository (default: current directory)",
|
|
167
|
+
)
|
|
168
|
+
server_parser.add_argument(
|
|
169
|
+
"--claude",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Create Claude Code config before starting server",
|
|
172
|
+
)
|
|
173
|
+
server_parser.add_argument(
|
|
174
|
+
"--cursor",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Create Cursor config before starting server",
|
|
177
|
+
)
|
|
178
|
+
server_parser.add_argument(
|
|
179
|
+
"--vs",
|
|
180
|
+
action="store_true",
|
|
181
|
+
help="Create VS Code config before starting server",
|
|
182
|
+
)
|
|
183
|
+
server_parser.add_argument(
|
|
184
|
+
"--gemini",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="Create Gemini CLI config before starting server",
|
|
187
|
+
)
|
|
188
|
+
server_parser.add_argument(
|
|
189
|
+
"--codex",
|
|
190
|
+
action="store_true",
|
|
191
|
+
help="Create Codex config before starting server",
|
|
192
|
+
)
|
|
193
|
+
server_parser.add_argument(
|
|
194
|
+
"--fast",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="Fast tier: Regular extraction + lemmi expansion (if reindexing needed)",
|
|
197
|
+
)
|
|
198
|
+
server_parser.add_argument(
|
|
199
|
+
"--regular",
|
|
200
|
+
action="store_true",
|
|
201
|
+
help="Regular tier: KeyBERT small + GloVe expansion (if reindexing needed)",
|
|
202
|
+
)
|
|
203
|
+
server_parser.add_argument(
|
|
204
|
+
"--max",
|
|
205
|
+
action="store_true",
|
|
206
|
+
help="Max tier: KeyBERT large + FastText expansion (if reindexing needed)",
|
|
207
|
+
)
|
|
208
|
+
server_parser.add_argument(
|
|
209
|
+
"--watch",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Start file watcher in a linked process for automatic reindexing",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
claude_parser = subparsers.add_parser(
|
|
215
|
+
"claude",
|
|
216
|
+
help="Setup Cicada for Claude Code editor",
|
|
217
|
+
description="One-command setup for Claude Code with keyword extraction",
|
|
218
|
+
)
|
|
219
|
+
claude_parser.add_argument(
|
|
220
|
+
"--fast",
|
|
221
|
+
action="store_true",
|
|
222
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
223
|
+
)
|
|
224
|
+
claude_parser.add_argument(
|
|
225
|
+
"--regular",
|
|
226
|
+
action="store_true",
|
|
227
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
228
|
+
)
|
|
229
|
+
claude_parser.add_argument(
|
|
230
|
+
"--max",
|
|
231
|
+
action="store_true",
|
|
232
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
cursor_parser = subparsers.add_parser(
|
|
236
|
+
"cursor",
|
|
237
|
+
help="Setup Cicada for Cursor editor",
|
|
238
|
+
description="One-command setup for Cursor with keyword extraction",
|
|
239
|
+
)
|
|
240
|
+
cursor_parser.add_argument(
|
|
241
|
+
"--fast",
|
|
242
|
+
action="store_true",
|
|
243
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
244
|
+
)
|
|
245
|
+
cursor_parser.add_argument(
|
|
246
|
+
"--regular",
|
|
247
|
+
action="store_true",
|
|
248
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
249
|
+
)
|
|
250
|
+
cursor_parser.add_argument(
|
|
251
|
+
"--max",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
vs_parser = subparsers.add_parser(
|
|
257
|
+
"vs",
|
|
258
|
+
help="Setup Cicada for VS Code editor",
|
|
259
|
+
description="One-command setup for VS Code with keyword extraction",
|
|
260
|
+
)
|
|
261
|
+
vs_parser.add_argument(
|
|
262
|
+
"--fast",
|
|
263
|
+
action="store_true",
|
|
264
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
265
|
+
)
|
|
266
|
+
vs_parser.add_argument(
|
|
267
|
+
"--regular",
|
|
268
|
+
action="store_true",
|
|
269
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
270
|
+
)
|
|
271
|
+
vs_parser.add_argument(
|
|
272
|
+
"--max",
|
|
273
|
+
action="store_true",
|
|
274
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
gemini_parser = subparsers.add_parser(
|
|
278
|
+
"gemini",
|
|
279
|
+
help="Setup Cicada for Gemini CLI",
|
|
280
|
+
description="One-command setup for Gemini CLI with keyword extraction",
|
|
281
|
+
)
|
|
282
|
+
gemini_parser.add_argument(
|
|
283
|
+
"--fast",
|
|
284
|
+
action="store_true",
|
|
285
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
286
|
+
)
|
|
287
|
+
gemini_parser.add_argument(
|
|
288
|
+
"--regular",
|
|
289
|
+
action="store_true",
|
|
290
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
291
|
+
)
|
|
292
|
+
gemini_parser.add_argument(
|
|
293
|
+
"--max",
|
|
294
|
+
action="store_true",
|
|
295
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
codex_parser = subparsers.add_parser(
|
|
299
|
+
"codex",
|
|
300
|
+
help="Setup Cicada for Codex editor",
|
|
301
|
+
description="One-command setup for Codex with keyword extraction",
|
|
302
|
+
)
|
|
303
|
+
codex_parser.add_argument(
|
|
304
|
+
"--fast",
|
|
305
|
+
action="store_true",
|
|
306
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
307
|
+
)
|
|
308
|
+
codex_parser.add_argument(
|
|
309
|
+
"--regular",
|
|
310
|
+
action="store_true",
|
|
311
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
312
|
+
)
|
|
313
|
+
codex_parser.add_argument(
|
|
314
|
+
"--max",
|
|
315
|
+
action="store_true",
|
|
316
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
watch_parser = subparsers.add_parser(
|
|
320
|
+
"watch",
|
|
321
|
+
help="Watch for file changes and automatically reindex",
|
|
322
|
+
description="Watch Elixir source files for changes and trigger automatic incremental reindexing",
|
|
323
|
+
)
|
|
324
|
+
watch_parser.add_argument(
|
|
325
|
+
"repo",
|
|
326
|
+
nargs="?",
|
|
327
|
+
default=".",
|
|
328
|
+
help="Path to the Elixir repository to watch (default: current directory)",
|
|
329
|
+
)
|
|
330
|
+
watch_parser.add_argument(
|
|
331
|
+
"--debounce",
|
|
332
|
+
type=float,
|
|
333
|
+
default=2.0,
|
|
334
|
+
metavar="SECONDS",
|
|
335
|
+
help="Debounce interval in seconds to wait after file changes before reindexing (default: 2.0)",
|
|
336
|
+
)
|
|
337
|
+
watch_parser.add_argument(
|
|
338
|
+
"--fast",
|
|
339
|
+
action="store_true",
|
|
340
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
341
|
+
)
|
|
342
|
+
watch_parser.add_argument(
|
|
343
|
+
"--regular",
|
|
344
|
+
action="store_true",
|
|
345
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
346
|
+
)
|
|
347
|
+
watch_parser.add_argument(
|
|
348
|
+
"--max",
|
|
349
|
+
action="store_true",
|
|
350
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
index_parser = subparsers.add_parser(
|
|
354
|
+
"index",
|
|
355
|
+
help="Index an Elixir repository to extract modules and functions",
|
|
356
|
+
description="Index current Elixir repository to extract modules and functions",
|
|
357
|
+
)
|
|
358
|
+
index_parser.add_argument(
|
|
359
|
+
"repo",
|
|
360
|
+
nargs="?",
|
|
361
|
+
default=".",
|
|
362
|
+
help="Path to the Elixir repository to index (default: current directory)",
|
|
363
|
+
)
|
|
364
|
+
index_parser.add_argument(
|
|
365
|
+
"--fast",
|
|
366
|
+
action="store_true",
|
|
367
|
+
help="Fast tier: Regular extraction + lemmi expansion",
|
|
368
|
+
)
|
|
369
|
+
index_parser.add_argument(
|
|
370
|
+
"--regular",
|
|
371
|
+
action="store_true",
|
|
372
|
+
help="Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
373
|
+
)
|
|
374
|
+
index_parser.add_argument(
|
|
375
|
+
"--max",
|
|
376
|
+
action="store_true",
|
|
377
|
+
help="Max tier: KeyBERT large + FastText expansion",
|
|
378
|
+
)
|
|
379
|
+
index_parser.add_argument(
|
|
380
|
+
"-f",
|
|
381
|
+
"--force",
|
|
382
|
+
action="store_true",
|
|
383
|
+
help="Override configured tier (requires --fast, --regular, or --max)",
|
|
384
|
+
)
|
|
385
|
+
index_parser.add_argument(
|
|
386
|
+
"--test",
|
|
387
|
+
action="store_true",
|
|
388
|
+
help="Start interactive keyword extraction test mode",
|
|
389
|
+
)
|
|
390
|
+
index_parser.add_argument(
|
|
391
|
+
"--test-expansion",
|
|
392
|
+
action="store_true",
|
|
393
|
+
help="Start interactive keyword expansion test mode",
|
|
394
|
+
)
|
|
395
|
+
index_parser.add_argument(
|
|
396
|
+
"--extraction-threshold",
|
|
397
|
+
type=float,
|
|
398
|
+
default=0.3,
|
|
399
|
+
metavar="SCORE",
|
|
400
|
+
help="Minimum score for keyword extraction (0.0-1.0). For KeyBERT: semantic similarity threshold. Default: 0.3",
|
|
401
|
+
)
|
|
402
|
+
index_parser.add_argument(
|
|
403
|
+
"--min-score",
|
|
404
|
+
type=float,
|
|
405
|
+
default=0.5,
|
|
406
|
+
metavar="SCORE",
|
|
407
|
+
help="Minimum score threshold for keywords (filters out low-scoring terms). Default: 0.5",
|
|
408
|
+
)
|
|
409
|
+
index_parser.add_argument(
|
|
410
|
+
"--expansion-threshold",
|
|
411
|
+
type=float,
|
|
412
|
+
default=0.2,
|
|
413
|
+
metavar="SCORE",
|
|
414
|
+
help="Minimum similarity score for keyword expansion (0.0-1.0, default: 0.2)",
|
|
415
|
+
)
|
|
416
|
+
index_parser.add_argument(
|
|
417
|
+
"--watch",
|
|
418
|
+
action="store_true",
|
|
419
|
+
help="Watch for file changes and automatically reindex (runs initial index first)",
|
|
420
|
+
)
|
|
421
|
+
index_parser.add_argument(
|
|
422
|
+
"--debounce",
|
|
423
|
+
type=float,
|
|
424
|
+
default=2.0,
|
|
425
|
+
metavar="SECONDS",
|
|
426
|
+
help="Debounce interval in seconds when using --watch (default: 2.0)",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
index_pr_parser = subparsers.add_parser(
|
|
430
|
+
"index-pr",
|
|
431
|
+
help="Index GitHub pull requests for fast offline lookup",
|
|
432
|
+
description="Index GitHub pull requests for fast offline lookup",
|
|
433
|
+
)
|
|
434
|
+
index_pr_parser.add_argument(
|
|
435
|
+
"repo",
|
|
436
|
+
nargs="?",
|
|
437
|
+
default=".",
|
|
438
|
+
help="Path to git repository (default: current directory)",
|
|
439
|
+
)
|
|
440
|
+
index_pr_parser.add_argument(
|
|
441
|
+
"--clean",
|
|
442
|
+
action="store_true",
|
|
443
|
+
help="Clean and rebuild the entire index from scratch (default: incremental update)",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
dead_code_parser = subparsers.add_parser(
|
|
447
|
+
"find-dead-code",
|
|
448
|
+
help="Find potentially unused public functions in Elixir codebase",
|
|
449
|
+
description="Find potentially unused public functions in Elixir codebase",
|
|
450
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
451
|
+
epilog="""
|
|
452
|
+
Confidence Levels:
|
|
453
|
+
high - Zero usage, no dynamic call indicators, no behaviors/uses
|
|
454
|
+
medium - Zero usage, but module has behaviors or uses (possible callbacks)
|
|
455
|
+
low - Zero usage, but module passed as value (possible dynamic calls)
|
|
456
|
+
|
|
457
|
+
Examples:
|
|
458
|
+
cicada find-dead-code # Show high confidence candidates
|
|
459
|
+
cicada find-dead-code --min-confidence low # Show all candidates
|
|
460
|
+
cicada find-dead-code --format json # Output as JSON
|
|
461
|
+
""",
|
|
462
|
+
)
|
|
463
|
+
dead_code_parser.add_argument(
|
|
464
|
+
"--format",
|
|
465
|
+
choices=["markdown", "json"],
|
|
466
|
+
default="markdown",
|
|
467
|
+
help="Output format (default: markdown)",
|
|
468
|
+
)
|
|
469
|
+
dead_code_parser.add_argument(
|
|
470
|
+
"--min-confidence",
|
|
471
|
+
choices=["high", "medium", "low"],
|
|
472
|
+
default="high",
|
|
473
|
+
help="Minimum confidence level to show (default: high)",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
clean_parser = subparsers.add_parser(
|
|
477
|
+
"clean",
|
|
478
|
+
help="Remove Cicada configuration and indexes",
|
|
479
|
+
description="Remove Cicada configuration and indexes for current repository",
|
|
480
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
481
|
+
epilog="""
|
|
482
|
+
Examples:
|
|
483
|
+
cicada clean # Remove everything (interactive with confirmation)
|
|
484
|
+
cicada clean -f # Remove everything (skip confirmation)
|
|
485
|
+
cicada clean --index # Remove main index (index.json, hashes.json)
|
|
486
|
+
cicada clean --pr-index # Remove PR index (pr_index.json)
|
|
487
|
+
cicada clean --all # Remove ALL project storage
|
|
488
|
+
cicada clean --all -f # Remove ALL project storage (skip confirmation)
|
|
489
|
+
""",
|
|
490
|
+
)
|
|
491
|
+
clean_parser.add_argument(
|
|
492
|
+
"-f",
|
|
493
|
+
"--force",
|
|
494
|
+
action="store_true",
|
|
495
|
+
help="Skip confirmation prompt (for full clean or --all)",
|
|
496
|
+
)
|
|
497
|
+
clean_parser.add_argument(
|
|
498
|
+
"--index",
|
|
499
|
+
action="store_true",
|
|
500
|
+
help="Remove only main index files (index.json, hashes.json)",
|
|
501
|
+
)
|
|
502
|
+
clean_parser.add_argument(
|
|
503
|
+
"--pr-index",
|
|
504
|
+
action="store_true",
|
|
505
|
+
help="Remove only PR index file (pr_index.json)",
|
|
506
|
+
)
|
|
507
|
+
clean_parser.add_argument(
|
|
508
|
+
"--all",
|
|
509
|
+
action="store_true",
|
|
510
|
+
help="Remove ALL Cicada storage for all projects (~/.cicada/projects/)",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
dir_parser = subparsers.add_parser(
|
|
514
|
+
"dir",
|
|
515
|
+
help="Show the absolute path to the Cicada storage directory",
|
|
516
|
+
description="Display the absolute path to where Cicada stores configuration and indexes",
|
|
517
|
+
)
|
|
518
|
+
dir_parser.add_argument(
|
|
519
|
+
"repo",
|
|
520
|
+
nargs="?",
|
|
521
|
+
default=".",
|
|
522
|
+
help="Path to the repository (default: current directory)",
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return parser
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def handle_command(args) -> bool:
|
|
529
|
+
"""Route command to appropriate handler.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
args: Parsed command-line arguments
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
True if a command was handled, False if no command specified
|
|
536
|
+
"""
|
|
537
|
+
command_handlers = {
|
|
538
|
+
"install": handle_install,
|
|
539
|
+
"server": handle_server,
|
|
540
|
+
"claude": lambda args: handle_editor_setup(args, "claude"),
|
|
541
|
+
"cursor": lambda args: handle_editor_setup(args, "cursor"),
|
|
542
|
+
"vs": lambda args: handle_editor_setup(args, "vs"),
|
|
543
|
+
"gemini": lambda args: handle_editor_setup(args, "gemini"),
|
|
544
|
+
"codex": lambda args: handle_editor_setup(args, "codex"),
|
|
545
|
+
"watch": handle_watch,
|
|
546
|
+
"index": handle_index,
|
|
547
|
+
"index-pr": handle_index_pr,
|
|
548
|
+
"find-dead-code": handle_find_dead_code,
|
|
549
|
+
"clean": handle_clean,
|
|
550
|
+
"dir": handle_dir,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if args.command is None:
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
handler = command_handlers.get(args.command)
|
|
557
|
+
if handler:
|
|
558
|
+
handler(args)
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def handle_editor_setup(args, editor: str) -> None:
|
|
565
|
+
"""Handle setup for a specific editor.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
args: Parsed command-line arguments
|
|
569
|
+
editor: Editor type ('claude', 'cursor', or 'vs')
|
|
570
|
+
"""
|
|
571
|
+
from typing import cast
|
|
572
|
+
|
|
573
|
+
from cicada.setup import EditorType, setup
|
|
574
|
+
from cicada.utils.storage import get_config_path, get_index_path
|
|
575
|
+
|
|
576
|
+
# Validate tier flags
|
|
577
|
+
validate_tier_flags(args)
|
|
578
|
+
|
|
579
|
+
repo_path = Path.cwd()
|
|
580
|
+
|
|
581
|
+
# Verify it's an Elixir project
|
|
582
|
+
if not (repo_path / "mix.exs").exists():
|
|
583
|
+
print(f"Error: {repo_path} does not appear to be an Elixir project", file=sys.stderr)
|
|
584
|
+
print("(mix.exs not found)", file=sys.stderr)
|
|
585
|
+
sys.exit(1)
|
|
586
|
+
|
|
587
|
+
config_path = get_config_path(repo_path)
|
|
588
|
+
index_path = get_index_path(repo_path)
|
|
589
|
+
index_exists = config_path.exists() and index_path.exists()
|
|
590
|
+
|
|
591
|
+
extraction_method, expansion_method = get_extraction_expansion_methods(args)
|
|
592
|
+
|
|
593
|
+
# Load existing config if no tier specified but index exists
|
|
594
|
+
if extraction_method is None and index_exists:
|
|
595
|
+
extraction_method, expansion_method = _load_existing_config(config_path)
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
assert editor is not None
|
|
599
|
+
setup(
|
|
600
|
+
cast(EditorType, editor),
|
|
601
|
+
repo_path,
|
|
602
|
+
extraction_method=extraction_method,
|
|
603
|
+
expansion_method=expansion_method,
|
|
604
|
+
index_exists=index_exists,
|
|
605
|
+
)
|
|
606
|
+
except Exception as e:
|
|
607
|
+
print(f"\nError: Setup failed: {e}", file=sys.stderr)
|
|
608
|
+
sys.exit(1)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _load_existing_config(config_path: Path) -> tuple[str, str]:
|
|
612
|
+
"""Load extraction and expansion methods from existing config.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
config_path: Path to config.yaml
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
Tuple of (extraction_method, expansion_method)
|
|
619
|
+
"""
|
|
620
|
+
import yaml
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
with open(config_path) as f:
|
|
624
|
+
existing_config = yaml.safe_load(f)
|
|
625
|
+
extraction_method = existing_config.get("keyword_extraction", {}).get(
|
|
626
|
+
"method", "regular"
|
|
627
|
+
)
|
|
628
|
+
expansion_method = existing_config.get("keyword_expansion", {}).get("method", "lemmi")
|
|
629
|
+
return extraction_method, expansion_method
|
|
630
|
+
except Exception as e:
|
|
631
|
+
print(f"Warning: Could not load existing config: {e}", file=sys.stderr)
|
|
632
|
+
return "regular", "lemmi"
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def handle_index_test_mode(args):
|
|
636
|
+
"""Handle interactive keyword extraction test mode."""
|
|
637
|
+
from cicada.keyword_test import run_keywords_interactive
|
|
638
|
+
from cicada.tier import determine_tier, tier_to_methods
|
|
639
|
+
|
|
640
|
+
# Validate tier flags
|
|
641
|
+
validate_tier_flags(args)
|
|
642
|
+
|
|
643
|
+
# Get tier (includes fallback to 'regular' if not specified)
|
|
644
|
+
tier_name = determine_tier(args)
|
|
645
|
+
|
|
646
|
+
# Convert tier to extraction method
|
|
647
|
+
extraction_method, _ = tier_to_methods(tier_name)
|
|
648
|
+
|
|
649
|
+
extraction_threshold = getattr(args, "extraction_threshold", None)
|
|
650
|
+
run_keywords_interactive(
|
|
651
|
+
method=extraction_method, tier=tier_name, extraction_threshold=extraction_threshold
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def handle_index_test_expansion_mode(args):
|
|
656
|
+
"""Handle interactive keyword expansion test mode."""
|
|
657
|
+
from cicada.keyword_test import run_expansion_interactive
|
|
658
|
+
from cicada.tier import determine_tier, tier_to_methods
|
|
659
|
+
|
|
660
|
+
# Validate tier flags
|
|
661
|
+
validate_tier_flags(args)
|
|
662
|
+
|
|
663
|
+
# Get tier (includes fallback to 'regular' if not specified)
|
|
664
|
+
tier_name = determine_tier(args)
|
|
665
|
+
|
|
666
|
+
# Convert tier to extraction method and expansion type
|
|
667
|
+
extraction_method, expansion_type = tier_to_methods(tier_name)
|
|
668
|
+
|
|
669
|
+
extraction_threshold = getattr(args, "extraction_threshold", 0.3)
|
|
670
|
+
expansion_threshold = getattr(args, "expansion_threshold", 0.2)
|
|
671
|
+
min_score = getattr(args, "min_score", 0.5)
|
|
672
|
+
run_expansion_interactive(
|
|
673
|
+
expansion_type=expansion_type,
|
|
674
|
+
extraction_method=extraction_method,
|
|
675
|
+
extraction_tier=tier_name,
|
|
676
|
+
extraction_threshold=extraction_threshold,
|
|
677
|
+
expansion_threshold=expansion_threshold,
|
|
678
|
+
min_score=min_score,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def handle_index_main(args) -> None:
|
|
683
|
+
"""Handle main repository indexing."""
|
|
684
|
+
from cicada.indexer import ElixirIndexer
|
|
685
|
+
from cicada.utils.storage import create_storage_dir, get_config_path, get_index_path
|
|
686
|
+
|
|
687
|
+
# Validate tier flags
|
|
688
|
+
validate_tier_flags(args, require_force=True)
|
|
689
|
+
|
|
690
|
+
repo_path = Path(args.repo).resolve()
|
|
691
|
+
config_path = get_config_path(repo_path)
|
|
692
|
+
storage_dir = create_storage_dir(repo_path)
|
|
693
|
+
index_path = get_index_path(repo_path)
|
|
694
|
+
|
|
695
|
+
force_enabled = getattr(args, "force", False) is True
|
|
696
|
+
extraction_method: str | None = None
|
|
697
|
+
expansion_method: str | None = None
|
|
698
|
+
|
|
699
|
+
if force_enabled:
|
|
700
|
+
extraction_method, expansion_method = get_extraction_expansion_methods(args)
|
|
701
|
+
assert extraction_method is not None
|
|
702
|
+
assert expansion_method is not None
|
|
703
|
+
_handle_index_config_update(
|
|
704
|
+
config_path, storage_dir, repo_path, extraction_method, expansion_method
|
|
705
|
+
)
|
|
706
|
+
elif not config_path.exists():
|
|
707
|
+
_print_tier_requirement_error()
|
|
708
|
+
sys.exit(2)
|
|
709
|
+
|
|
710
|
+
# Perform indexing
|
|
711
|
+
indexer = ElixirIndexer(verbose=True)
|
|
712
|
+
indexer.incremental_index_repository(
|
|
713
|
+
str(repo_path),
|
|
714
|
+
str(index_path),
|
|
715
|
+
extract_keywords=True,
|
|
716
|
+
force_full=False,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _handle_index_config_update(
|
|
721
|
+
config_path: Path,
|
|
722
|
+
storage_dir: Path,
|
|
723
|
+
repo_path: Path,
|
|
724
|
+
extraction_method: str,
|
|
725
|
+
expansion_method: str,
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Handle config creation or validation during indexing.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
config_path: Path to config.yaml
|
|
731
|
+
storage_dir: Storage directory path
|
|
732
|
+
repo_path: Repository path
|
|
733
|
+
extraction_method: Extraction method to use
|
|
734
|
+
expansion_method: Expansion method to use
|
|
735
|
+
"""
|
|
736
|
+
from cicada.setup import create_config_yaml
|
|
737
|
+
|
|
738
|
+
if config_path.exists():
|
|
739
|
+
existing_extraction, existing_expansion = _load_existing_config(config_path)
|
|
740
|
+
|
|
741
|
+
extraction_changed = existing_extraction != extraction_method
|
|
742
|
+
expansion_changed = existing_expansion != expansion_method
|
|
743
|
+
|
|
744
|
+
if extraction_changed or expansion_changed:
|
|
745
|
+
_print_config_change_error(
|
|
746
|
+
existing_extraction,
|
|
747
|
+
existing_expansion,
|
|
748
|
+
extraction_method,
|
|
749
|
+
expansion_method,
|
|
750
|
+
extraction_changed,
|
|
751
|
+
expansion_changed,
|
|
752
|
+
)
|
|
753
|
+
sys.exit(1)
|
|
754
|
+
|
|
755
|
+
create_config_yaml(repo_path, storage_dir, extraction_method, expansion_method)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _print_config_change_error(
|
|
759
|
+
existing_extraction: str,
|
|
760
|
+
existing_expansion: str,
|
|
761
|
+
extraction_method: str,
|
|
762
|
+
expansion_method: str,
|
|
763
|
+
extraction_changed: bool,
|
|
764
|
+
expansion_changed: bool,
|
|
765
|
+
) -> None:
|
|
766
|
+
"""Print error message for config changes."""
|
|
767
|
+
change_desc = _describe_config_change(
|
|
768
|
+
existing_extraction,
|
|
769
|
+
existing_expansion,
|
|
770
|
+
extraction_method,
|
|
771
|
+
expansion_method,
|
|
772
|
+
extraction_changed,
|
|
773
|
+
expansion_changed,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
print(f"Error: Cannot change {change_desc}", file=sys.stderr)
|
|
777
|
+
print("\nTo reindex with different settings, first run:", file=sys.stderr)
|
|
778
|
+
print(" cicada clean", file=sys.stderr)
|
|
779
|
+
print("\nThen run your index command again.", file=sys.stderr)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _describe_config_change(
|
|
783
|
+
existing_extraction: str,
|
|
784
|
+
existing_expansion: str,
|
|
785
|
+
extraction_method: str,
|
|
786
|
+
expansion_method: str,
|
|
787
|
+
extraction_changed: bool,
|
|
788
|
+
expansion_changed: bool,
|
|
789
|
+
) -> str:
|
|
790
|
+
"""Generate description of config change."""
|
|
791
|
+
if extraction_changed and expansion_changed:
|
|
792
|
+
return f"extraction from {existing_extraction} to {extraction_method} and expansion from {existing_expansion} to {expansion_method}"
|
|
793
|
+
if extraction_changed:
|
|
794
|
+
return f"extraction from {existing_extraction} to {extraction_method}"
|
|
795
|
+
return f"expansion from {existing_expansion} to {expansion_method}"
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _print_tier_requirement_error() -> None:
|
|
799
|
+
"""Print error message when no tier is specified."""
|
|
800
|
+
print("Error: No tier configured.", file=sys.stderr)
|
|
801
|
+
print(
|
|
802
|
+
"\nUse '--force' with a tier flag to select keyword extraction settings:", file=sys.stderr
|
|
803
|
+
)
|
|
804
|
+
print(
|
|
805
|
+
" cicada index --force --fast Fast tier: Regular extraction + lemmi expansion",
|
|
806
|
+
file=sys.stderr,
|
|
807
|
+
)
|
|
808
|
+
print(
|
|
809
|
+
" cicada index --force --regular Regular tier: KeyBERT small + GloVe expansion (default)",
|
|
810
|
+
file=sys.stderr,
|
|
811
|
+
)
|
|
812
|
+
print(
|
|
813
|
+
" cicada index --force --max Max tier: KeyBERT large + FastText expansion",
|
|
814
|
+
file=sys.stderr,
|
|
815
|
+
)
|
|
816
|
+
print("\nRun 'cicada index --help' for more information.", file=sys.stderr)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def handle_index(args):
|
|
820
|
+
"""Route index command to appropriate handler based on mode."""
|
|
821
|
+
from cicada.version_check import check_for_updates
|
|
822
|
+
|
|
823
|
+
check_for_updates()
|
|
824
|
+
|
|
825
|
+
if getattr(args, "test", False):
|
|
826
|
+
handle_index_test_mode(args)
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
if getattr(args, "test_expansion", False):
|
|
830
|
+
handle_index_test_expansion_mode(args)
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
if getattr(args, "watch", False):
|
|
834
|
+
# Handle watch mode using shared logic
|
|
835
|
+
_setup_and_start_watcher(args, args.repo)
|
|
836
|
+
else:
|
|
837
|
+
handle_index_main(args)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def handle_watch(args):
|
|
841
|
+
"""Handle watch command for automatic reindexing on file changes."""
|
|
842
|
+
from cicada.version_check import check_for_updates
|
|
843
|
+
|
|
844
|
+
check_for_updates()
|
|
845
|
+
|
|
846
|
+
# Use shared watcher setup logic
|
|
847
|
+
_setup_and_start_watcher(args, args.repo)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def handle_index_pr(args):
|
|
851
|
+
from cicada.pr_indexer import PRIndexer
|
|
852
|
+
from cicada.utils import get_pr_index_path
|
|
853
|
+
from cicada.version_check import check_for_updates
|
|
854
|
+
|
|
855
|
+
check_for_updates()
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
output_path = str(get_pr_index_path(args.repo))
|
|
859
|
+
|
|
860
|
+
indexer = PRIndexer(repo_path=args.repo)
|
|
861
|
+
indexer.index_repository(output_path=output_path, incremental=not args.clean)
|
|
862
|
+
|
|
863
|
+
print("\n✅ Indexing complete! You can now use the MCP tools for PR history lookups.")
|
|
864
|
+
|
|
865
|
+
except KeyboardInterrupt:
|
|
866
|
+
print("\n\n⚠️ Indexing interrupted by user.")
|
|
867
|
+
print("Partial index may have been saved. Run again to continue (incremental by default).")
|
|
868
|
+
sys.exit(130)
|
|
869
|
+
|
|
870
|
+
except Exception as e:
|
|
871
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
872
|
+
sys.exit(1)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def handle_find_dead_code(args):
|
|
876
|
+
from cicada.dead_code.analyzer import DeadCodeAnalyzer
|
|
877
|
+
from cicada.dead_code.finder import filter_by_confidence, format_json, format_markdown
|
|
878
|
+
from cicada.utils import get_index_path, load_index
|
|
879
|
+
|
|
880
|
+
index_path = get_index_path(".")
|
|
881
|
+
|
|
882
|
+
if not index_path.exists():
|
|
883
|
+
print(f"Error: Index file not found: {index_path}", file=sys.stderr)
|
|
884
|
+
print("\nRun 'cicada index' first to create the index.", file=sys.stderr)
|
|
885
|
+
sys.exit(1)
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
index = load_index(index_path, raise_on_error=True)
|
|
889
|
+
except Exception as e:
|
|
890
|
+
print(f"Error loading index: {e}", file=sys.stderr)
|
|
891
|
+
sys.exit(1)
|
|
892
|
+
|
|
893
|
+
assert index is not None, "Index should not be None after successful load"
|
|
894
|
+
|
|
895
|
+
analyzer = DeadCodeAnalyzer(index)
|
|
896
|
+
results = analyzer.analyze()
|
|
897
|
+
|
|
898
|
+
results = filter_by_confidence(results, args.min_confidence)
|
|
899
|
+
|
|
900
|
+
output = format_json(results) if args.format == "json" else format_markdown(results)
|
|
901
|
+
|
|
902
|
+
print(output)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def handle_clean(args):
|
|
906
|
+
from cicada.clean import (
|
|
907
|
+
clean_all_projects,
|
|
908
|
+
clean_index_only,
|
|
909
|
+
clean_pr_index_only,
|
|
910
|
+
clean_repository,
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
if args.all:
|
|
914
|
+
try:
|
|
915
|
+
clean_all_projects(force=args.force)
|
|
916
|
+
except Exception as e:
|
|
917
|
+
print(f"\nError: Cleanup failed: {e}", file=sys.stderr)
|
|
918
|
+
sys.exit(1)
|
|
919
|
+
return
|
|
920
|
+
|
|
921
|
+
flag_count = sum([args.index, args.pr_index])
|
|
922
|
+
if flag_count > 1:
|
|
923
|
+
print("Error: Cannot specify multiple clean options.", file=sys.stderr)
|
|
924
|
+
print("Choose only one: --index, --pr-index, or -f/--force", file=sys.stderr)
|
|
925
|
+
sys.exit(1)
|
|
926
|
+
|
|
927
|
+
repo_path = Path.cwd()
|
|
928
|
+
|
|
929
|
+
try:
|
|
930
|
+
if args.index:
|
|
931
|
+
clean_index_only(repo_path)
|
|
932
|
+
elif args.pr_index:
|
|
933
|
+
clean_pr_index_only(repo_path)
|
|
934
|
+
else:
|
|
935
|
+
clean_repository(repo_path, force=args.force)
|
|
936
|
+
except Exception as e:
|
|
937
|
+
print(f"\nError: Cleanup failed: {e}", file=sys.stderr)
|
|
938
|
+
sys.exit(1)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def handle_dir(args):
|
|
942
|
+
"""Show the absolute path to the Cicada storage directory."""
|
|
943
|
+
from cicada.utils.storage import get_storage_dir
|
|
944
|
+
|
|
945
|
+
repo_path = Path(args.repo).resolve()
|
|
946
|
+
|
|
947
|
+
try:
|
|
948
|
+
storage_dir = get_storage_dir(repo_path)
|
|
949
|
+
print(str(storage_dir))
|
|
950
|
+
except Exception as e:
|
|
951
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
952
|
+
sys.exit(1)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def handle_install(args) -> None:
|
|
956
|
+
"""
|
|
957
|
+
Handle the install subcommand (interactive setup).
|
|
958
|
+
|
|
959
|
+
Behavior:
|
|
960
|
+
- INTERACTIVE: shows prompts and menus
|
|
961
|
+
- Can skip prompts with flags (--claude, --cursor, --vs, --fast, --regular, --max)
|
|
962
|
+
- Creates editor config and indexes repository
|
|
963
|
+
"""
|
|
964
|
+
from typing import cast
|
|
965
|
+
|
|
966
|
+
from cicada.setup import EditorType, setup
|
|
967
|
+
from cicada.utils import get_config_path, get_index_path
|
|
968
|
+
|
|
969
|
+
# Determine and validate repository path
|
|
970
|
+
repo_path = Path(args.repo).resolve() if args.repo else Path.cwd().resolve()
|
|
971
|
+
_validate_elixir_project(repo_path)
|
|
972
|
+
|
|
973
|
+
# Validate tier flags
|
|
974
|
+
validate_tier_flags(args)
|
|
975
|
+
|
|
976
|
+
# Parse editor selection
|
|
977
|
+
editor = _determine_editor_from_args(args)
|
|
978
|
+
|
|
979
|
+
# Determine extraction and expansion methods from flags
|
|
980
|
+
extraction_method, expansion_method = get_extraction_expansion_methods(args)
|
|
981
|
+
|
|
982
|
+
# Check if index already exists
|
|
983
|
+
config_path = get_config_path(repo_path)
|
|
984
|
+
index_path = get_index_path(repo_path)
|
|
985
|
+
index_exists = config_path.exists() and index_path.exists()
|
|
986
|
+
|
|
987
|
+
# If no flags provided, use full interactive setup
|
|
988
|
+
if editor is None and extraction_method is None:
|
|
989
|
+
from cicada.interactive_setup import show_full_interactive_setup
|
|
990
|
+
|
|
991
|
+
show_full_interactive_setup(repo_path)
|
|
992
|
+
return
|
|
993
|
+
|
|
994
|
+
# If only model flags provided (no editor), prompt for editor
|
|
995
|
+
if editor is None:
|
|
996
|
+
editor = _prompt_for_editor()
|
|
997
|
+
|
|
998
|
+
# If only editor flag provided (no model), prompt for model (unless index exists)
|
|
999
|
+
if extraction_method is None and not index_exists:
|
|
1000
|
+
from cicada.interactive_setup import show_first_time_setup
|
|
1001
|
+
|
|
1002
|
+
extraction_method, expansion_method, _, _ = show_first_time_setup()
|
|
1003
|
+
|
|
1004
|
+
# If index exists but no model flags, use existing settings
|
|
1005
|
+
if extraction_method is None and index_exists:
|
|
1006
|
+
extraction_method, expansion_method = _load_existing_config(config_path)
|
|
1007
|
+
|
|
1008
|
+
# Run setup
|
|
1009
|
+
assert editor is not None
|
|
1010
|
+
try:
|
|
1011
|
+
setup(
|
|
1012
|
+
cast(EditorType, editor),
|
|
1013
|
+
repo_path,
|
|
1014
|
+
extraction_method=extraction_method,
|
|
1015
|
+
expansion_method=expansion_method,
|
|
1016
|
+
index_exists=index_exists,
|
|
1017
|
+
)
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
print(f"\nError: Setup failed: {e}", file=sys.stderr)
|
|
1020
|
+
sys.exit(1)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _validate_elixir_project(repo_path: Path) -> None:
|
|
1024
|
+
"""Validate that the repository is an Elixir project.
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
repo_path: Path to the repository
|
|
1028
|
+
|
|
1029
|
+
Raises:
|
|
1030
|
+
SystemExit: If not an Elixir project
|
|
1031
|
+
"""
|
|
1032
|
+
if not (repo_path / "mix.exs").exists():
|
|
1033
|
+
print(f"Error: {repo_path} does not appear to be an Elixir project", file=sys.stderr)
|
|
1034
|
+
print("(mix.exs not found)", file=sys.stderr)
|
|
1035
|
+
sys.exit(1)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _determine_editor_from_args(args) -> str | None:
|
|
1039
|
+
"""Determine editor from command-line arguments.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
args: Parsed command-line arguments
|
|
1043
|
+
|
|
1044
|
+
Returns:
|
|
1045
|
+
Editor type or None if not specified
|
|
1046
|
+
|
|
1047
|
+
Raises:
|
|
1048
|
+
SystemExit: If multiple editor flags specified
|
|
1049
|
+
"""
|
|
1050
|
+
editor_flags = [args.claude, args.cursor, args.vs, args.gemini, args.codex]
|
|
1051
|
+
editor_count = sum(editor_flags)
|
|
1052
|
+
|
|
1053
|
+
if editor_count > 1:
|
|
1054
|
+
print("Error: Can only specify one editor flag for install command", file=sys.stderr)
|
|
1055
|
+
sys.exit(1)
|
|
1056
|
+
|
|
1057
|
+
if args.claude:
|
|
1058
|
+
return "claude"
|
|
1059
|
+
if args.cursor:
|
|
1060
|
+
return "cursor"
|
|
1061
|
+
if args.vs:
|
|
1062
|
+
return "vs"
|
|
1063
|
+
if args.gemini:
|
|
1064
|
+
return "gemini"
|
|
1065
|
+
if args.codex:
|
|
1066
|
+
return "codex"
|
|
1067
|
+
return None
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _prompt_for_editor() -> str:
|
|
1071
|
+
"""Prompt user to select an editor.
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
Selected editor type
|
|
1075
|
+
|
|
1076
|
+
Raises:
|
|
1077
|
+
SystemExit: If user cancels selection
|
|
1078
|
+
"""
|
|
1079
|
+
from simple_term_menu import TerminalMenu
|
|
1080
|
+
|
|
1081
|
+
print("Select editor to configure:")
|
|
1082
|
+
print()
|
|
1083
|
+
editor_options = [
|
|
1084
|
+
"Claude Code (Claude AI assistant)",
|
|
1085
|
+
"Cursor (AI-powered code editor)",
|
|
1086
|
+
"VS Code (Visual Studio Code)",
|
|
1087
|
+
"Gemini CLI (Google Gemini command line interface)",
|
|
1088
|
+
"Codex (AI code editor)",
|
|
1089
|
+
]
|
|
1090
|
+
editor_menu = TerminalMenu(editor_options, title="Choose your editor:")
|
|
1091
|
+
menu_idx = editor_menu.show()
|
|
1092
|
+
|
|
1093
|
+
if menu_idx is None:
|
|
1094
|
+
print("\nSetup cancelled.")
|
|
1095
|
+
sys.exit(0)
|
|
1096
|
+
|
|
1097
|
+
# Map menu index to editor type
|
|
1098
|
+
assert isinstance(menu_idx, int), "menu_idx must be an integer"
|
|
1099
|
+
editor_map: tuple[str, str, str, str, str] = ("claude", "cursor", "vs", "gemini", "codex")
|
|
1100
|
+
return editor_map[menu_idx]
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def handle_server(args) -> None:
|
|
1104
|
+
"""
|
|
1105
|
+
Handle the server subcommand (silent MCP server with optional configs).
|
|
1106
|
+
|
|
1107
|
+
Behavior:
|
|
1108
|
+
- SILENT: no prompts, no interactive menus
|
|
1109
|
+
- Auto-setup if needed (uses default model: lemminflect)
|
|
1110
|
+
- Creates editor configs if flags provided (--claude, --cursor, --vs)
|
|
1111
|
+
- Starts MCP server on stdio
|
|
1112
|
+
"""
|
|
1113
|
+
import asyncio
|
|
1114
|
+
import logging
|
|
1115
|
+
|
|
1116
|
+
from cicada.utils import create_storage_dir, get_config_path, get_index_path
|
|
1117
|
+
|
|
1118
|
+
logger = logging.getLogger(__name__)
|
|
1119
|
+
|
|
1120
|
+
# Determine and validate repository path
|
|
1121
|
+
repo_path = Path(args.repo).resolve() if args.repo else Path.cwd().resolve()
|
|
1122
|
+
_validate_elixir_project(repo_path)
|
|
1123
|
+
|
|
1124
|
+
# Validate tier flags
|
|
1125
|
+
validate_tier_flags(args)
|
|
1126
|
+
|
|
1127
|
+
# Create storage directory
|
|
1128
|
+
storage_dir = create_storage_dir(repo_path)
|
|
1129
|
+
|
|
1130
|
+
# Determine extraction and expansion methods
|
|
1131
|
+
extraction_method, expansion_method = get_extraction_expansion_methods(args)
|
|
1132
|
+
|
|
1133
|
+
# Check if setup is needed
|
|
1134
|
+
config_path = get_config_path(repo_path)
|
|
1135
|
+
index_path = get_index_path(repo_path)
|
|
1136
|
+
needs_setup = not (config_path.exists() and index_path.exists())
|
|
1137
|
+
|
|
1138
|
+
if needs_setup:
|
|
1139
|
+
_perform_silent_setup(repo_path, storage_dir, extraction_method, expansion_method)
|
|
1140
|
+
|
|
1141
|
+
# Create editor configs if requested
|
|
1142
|
+
_configure_editors_if_requested(args, repo_path, storage_dir)
|
|
1143
|
+
|
|
1144
|
+
# Start watch process if requested
|
|
1145
|
+
watch_enabled = getattr(args, "watch", False)
|
|
1146
|
+
if watch_enabled:
|
|
1147
|
+
_start_watch_for_server(args, repo_path)
|
|
1148
|
+
|
|
1149
|
+
# Start MCP server
|
|
1150
|
+
from cicada.mcp.server import async_main
|
|
1151
|
+
|
|
1152
|
+
try:
|
|
1153
|
+
asyncio.run(async_main())
|
|
1154
|
+
finally:
|
|
1155
|
+
# Ensure watch process is stopped when server exits
|
|
1156
|
+
if watch_enabled:
|
|
1157
|
+
_cleanup_watch_process(logger)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _perform_silent_setup(
|
|
1161
|
+
repo_path: Path, storage_dir: Path, extraction_method: str | None, expansion_method: str | None
|
|
1162
|
+
) -> None:
|
|
1163
|
+
"""Perform silent setup with defaults if needed.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
repo_path: Repository path
|
|
1167
|
+
storage_dir: Storage directory path
|
|
1168
|
+
extraction_method: Extraction method or None for defaults
|
|
1169
|
+
expansion_method: Expansion method or None for defaults
|
|
1170
|
+
"""
|
|
1171
|
+
from cicada.setup import create_config_yaml, index_repository
|
|
1172
|
+
|
|
1173
|
+
# If no tier specified, default to fast tier (fastest, no downloads)
|
|
1174
|
+
if extraction_method is None:
|
|
1175
|
+
extraction_method = "regular"
|
|
1176
|
+
expansion_method = "lemmi"
|
|
1177
|
+
|
|
1178
|
+
# Create config.yaml (silent)
|
|
1179
|
+
create_config_yaml(repo_path, storage_dir, extraction_method, expansion_method, verbose=False)
|
|
1180
|
+
|
|
1181
|
+
# Index repository (silent)
|
|
1182
|
+
try:
|
|
1183
|
+
index_repository(repo_path, force_full=False, verbose=False)
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
print(f"Error during indexing: {e}", file=sys.stderr)
|
|
1186
|
+
sys.exit(1)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _configure_editors_if_requested(args, repo_path: Path, storage_dir: Path) -> None:
|
|
1190
|
+
"""Configure editors if flags are provided.
|
|
1191
|
+
|
|
1192
|
+
Args:
|
|
1193
|
+
args: Parsed command-line arguments
|
|
1194
|
+
repo_path: Repository path
|
|
1195
|
+
storage_dir: Storage directory path
|
|
1196
|
+
"""
|
|
1197
|
+
from cicada.setup import EditorType, setup_multiple_editors
|
|
1198
|
+
|
|
1199
|
+
editors_to_configure: list[EditorType] = []
|
|
1200
|
+
if args.claude:
|
|
1201
|
+
editors_to_configure.append("claude")
|
|
1202
|
+
if args.cursor:
|
|
1203
|
+
editors_to_configure.append("cursor")
|
|
1204
|
+
if args.vs:
|
|
1205
|
+
editors_to_configure.append("vs")
|
|
1206
|
+
if args.gemini:
|
|
1207
|
+
editors_to_configure.append("gemini")
|
|
1208
|
+
if args.codex:
|
|
1209
|
+
editors_to_configure.append("codex")
|
|
1210
|
+
|
|
1211
|
+
if editors_to_configure:
|
|
1212
|
+
try:
|
|
1213
|
+
setup_multiple_editors(editors_to_configure, repo_path, storage_dir, verbose=False)
|
|
1214
|
+
except Exception as e:
|
|
1215
|
+
print(f"Error creating editor configs: {e}", file=sys.stderr)
|
|
1216
|
+
sys.exit(1)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def _start_watch_for_server(args, repo_path: Path) -> None:
|
|
1220
|
+
"""Start watch process for the server.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
args: Parsed command-line arguments
|
|
1224
|
+
repo_path: Repository path
|
|
1225
|
+
"""
|
|
1226
|
+
from cicada.watch_manager import start_watch_process
|
|
1227
|
+
|
|
1228
|
+
# Determine tier using helper
|
|
1229
|
+
tier = determine_tier(args, repo_path)
|
|
1230
|
+
|
|
1231
|
+
# Start the watch process
|
|
1232
|
+
try:
|
|
1233
|
+
if not start_watch_process(repo_path, tier=tier, debounce=DEFAULT_WATCH_DEBOUNCE):
|
|
1234
|
+
print("ERROR: Failed to start watch process as requested", file=sys.stderr)
|
|
1235
|
+
print("Server startup aborted. Run without --watch or fix the issue.", file=sys.stderr)
|
|
1236
|
+
sys.exit(1)
|
|
1237
|
+
except RuntimeError as e:
|
|
1238
|
+
print(f"ERROR: Cannot start watch process: {e}", file=sys.stderr)
|
|
1239
|
+
print("Server startup aborted. Run without --watch or fix the issue.", file=sys.stderr)
|
|
1240
|
+
sys.exit(1)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _cleanup_watch_process(logger) -> None:
|
|
1244
|
+
"""Clean up watch process on server exit.
|
|
1245
|
+
|
|
1246
|
+
Args:
|
|
1247
|
+
logger: Logger instance
|
|
1248
|
+
"""
|
|
1249
|
+
try:
|
|
1250
|
+
from cicada.watch_manager import stop_watch_process
|
|
1251
|
+
|
|
1252
|
+
stop_watch_process()
|
|
1253
|
+
except Exception as e:
|
|
1254
|
+
logger.exception("Error stopping watch process during cleanup")
|
|
1255
|
+
print(f"Warning: Error stopping watch process: {e}", file=sys.stderr)
|