cicada-mcp 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cicada/ascii_art.py +60 -0
- cicada/clean.py +195 -60
- cicada/cli.py +757 -0
- cicada/colors.py +27 -0
- cicada/command_logger.py +14 -16
- cicada/dead_code_analyzer.py +12 -19
- cicada/extractors/__init__.py +6 -6
- cicada/extractors/base.py +3 -3
- cicada/extractors/call.py +11 -15
- cicada/extractors/dependency.py +39 -51
- cicada/extractors/doc.py +8 -9
- cicada/extractors/function.py +12 -24
- cicada/extractors/module.py +11 -15
- cicada/extractors/spec.py +8 -12
- cicada/find_dead_code.py +15 -39
- cicada/formatter.py +37 -91
- cicada/git_helper.py +22 -34
- cicada/indexer.py +122 -107
- cicada/interactive_setup.py +490 -0
- cicada/keybert_extractor.py +286 -0
- cicada/keyword_search.py +22 -30
- cicada/keyword_test.py +127 -0
- cicada/lightweight_keyword_extractor.py +5 -13
- cicada/mcp_entry.py +683 -0
- cicada/mcp_server.py +103 -209
- cicada/parser.py +9 -9
- cicada/pr_finder.py +15 -19
- cicada/pr_indexer/__init__.py +3 -3
- cicada/pr_indexer/cli.py +4 -9
- cicada/pr_indexer/github_api_client.py +22 -37
- cicada/pr_indexer/indexer.py +17 -29
- cicada/pr_indexer/line_mapper.py +8 -12
- cicada/pr_indexer/pr_index_builder.py +22 -34
- cicada/setup.py +189 -87
- cicada/utils/__init__.py +9 -9
- cicada/utils/call_site_formatter.py +4 -6
- cicada/utils/function_grouper.py +4 -4
- cicada/utils/hash_utils.py +12 -15
- cicada/utils/index_utils.py +15 -15
- cicada/utils/path_utils.py +24 -29
- cicada/utils/signature_builder.py +3 -3
- cicada/utils/subprocess_runner.py +17 -19
- cicada/utils/text_utils.py +1 -2
- cicada/version_check.py +2 -5
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
- cicada_mcp-0.2.0.dist-info/RECORD +53 -0
- cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
- cicada/install.py +0 -741
- cicada_mcp-0.1.7.dist-info/RECORD +0 -47
- cicada_mcp-0.1.7.dist-info/entry_points.txt +0 -9
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
cicada/cli.py
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified CLI entry point for Cicada.
|
|
3
|
+
|
|
4
|
+
Provides a single `cicada` command with multiple subcommands:
|
|
5
|
+
- cicada [path] - Setup/install Cicada for a project
|
|
6
|
+
- cicada claude - Setup Cicada for Claude Code editor
|
|
7
|
+
- cicada cursor - Setup Cicada for Cursor editor
|
|
8
|
+
- cicada vs - Setup Cicada for VS Code editor
|
|
9
|
+
- cicada index - Index an Elixir repository
|
|
10
|
+
- cicada index-pr - Index GitHub pull requests
|
|
11
|
+
- cicada find-dead-code - Find potentially unused functions
|
|
12
|
+
- cicada clean - Remove Cicada configuration and indexes
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
"""Main entry point for the unified cicada CLI."""
|
|
21
|
+
# Pre-process arguments for backward compatibility
|
|
22
|
+
# If first arg is not a known subcommand and looks like a path, inject "install"
|
|
23
|
+
if len(sys.argv) > 1:
|
|
24
|
+
first_arg = sys.argv[1]
|
|
25
|
+
known_commands = [
|
|
26
|
+
"install",
|
|
27
|
+
"server",
|
|
28
|
+
"claude",
|
|
29
|
+
"cursor",
|
|
30
|
+
"vs",
|
|
31
|
+
"index",
|
|
32
|
+
"index-pr",
|
|
33
|
+
"find-dead-code",
|
|
34
|
+
"clean",
|
|
35
|
+
]
|
|
36
|
+
# If first arg is not a known command and not a help flag, treat as path for install
|
|
37
|
+
if first_arg not in known_commands and not first_arg.startswith("-"):
|
|
38
|
+
# Insert 'install' as the subcommand
|
|
39
|
+
sys.argv.insert(1, "install")
|
|
40
|
+
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="cicada",
|
|
43
|
+
description="Cicada - AI-powered Elixir code analysis and search",
|
|
44
|
+
epilog="Run 'cicada <command> --help' for more information on a command.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Create subparsers for commands
|
|
48
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
49
|
+
|
|
50
|
+
# ========================================================================
|
|
51
|
+
# INSTALL subcommand - Interactive setup
|
|
52
|
+
# ========================================================================
|
|
53
|
+
install_parser = subparsers.add_parser(
|
|
54
|
+
"install",
|
|
55
|
+
help="Interactive setup for Cicada",
|
|
56
|
+
description="Interactive setup with editor and model selection",
|
|
57
|
+
)
|
|
58
|
+
install_parser.add_argument(
|
|
59
|
+
"repo",
|
|
60
|
+
nargs="?",
|
|
61
|
+
default=None,
|
|
62
|
+
help="Path to Elixir repository (default: current directory)",
|
|
63
|
+
)
|
|
64
|
+
install_parser.add_argument(
|
|
65
|
+
"--claude",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="Skip editor selection, use Claude Code",
|
|
68
|
+
)
|
|
69
|
+
install_parser.add_argument(
|
|
70
|
+
"--cursor",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Skip editor selection, use Cursor",
|
|
73
|
+
)
|
|
74
|
+
install_parser.add_argument(
|
|
75
|
+
"--vs",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Skip editor selection, use VS Code",
|
|
78
|
+
)
|
|
79
|
+
install_parser.add_argument(
|
|
80
|
+
"--nlp",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Skip model selection, use Lemminflect",
|
|
83
|
+
)
|
|
84
|
+
install_parser.add_argument(
|
|
85
|
+
"--rag",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Skip model selection, use BERT (default tier)",
|
|
88
|
+
)
|
|
89
|
+
install_parser.add_argument(
|
|
90
|
+
"--fast",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Use BERT fast tier (requires --rag)",
|
|
93
|
+
)
|
|
94
|
+
install_parser.add_argument(
|
|
95
|
+
"--max",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Use BERT max tier (requires --rag)",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# ========================================================================
|
|
101
|
+
# SERVER subcommand - Silent MCP server
|
|
102
|
+
# ========================================================================
|
|
103
|
+
server_parser = subparsers.add_parser(
|
|
104
|
+
"server",
|
|
105
|
+
help="Start MCP server (silent mode with defaults)",
|
|
106
|
+
description="Start MCP server with auto-setup using defaults",
|
|
107
|
+
)
|
|
108
|
+
server_parser.add_argument(
|
|
109
|
+
"repo",
|
|
110
|
+
nargs="?",
|
|
111
|
+
default=None,
|
|
112
|
+
help="Path to Elixir repository (default: current directory)",
|
|
113
|
+
)
|
|
114
|
+
server_parser.add_argument(
|
|
115
|
+
"--claude",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="Create Claude Code config before starting server",
|
|
118
|
+
)
|
|
119
|
+
server_parser.add_argument(
|
|
120
|
+
"--cursor",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Create Cursor config before starting server",
|
|
123
|
+
)
|
|
124
|
+
server_parser.add_argument(
|
|
125
|
+
"--vs",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="Create VS Code config before starting server",
|
|
128
|
+
)
|
|
129
|
+
server_parser.add_argument(
|
|
130
|
+
"--nlp",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="Force Lemminflect (if reindexing needed)",
|
|
133
|
+
)
|
|
134
|
+
server_parser.add_argument(
|
|
135
|
+
"--rag",
|
|
136
|
+
action="store_true",
|
|
137
|
+
help="Force BERT (if reindexing needed)",
|
|
138
|
+
)
|
|
139
|
+
server_parser.add_argument(
|
|
140
|
+
"--fast",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Force BERT fast tier (requires --rag)",
|
|
143
|
+
)
|
|
144
|
+
server_parser.add_argument(
|
|
145
|
+
"--max",
|
|
146
|
+
action="store_true",
|
|
147
|
+
help="Force BERT max tier (requires --rag)",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# ========================================================================
|
|
151
|
+
# CLAUDE subcommand (editor setup)
|
|
152
|
+
# ========================================================================
|
|
153
|
+
claude_parser = subparsers.add_parser(
|
|
154
|
+
"claude",
|
|
155
|
+
help="Setup Cicada for Claude Code editor",
|
|
156
|
+
description="One-command setup for Claude Code with keyword extraction",
|
|
157
|
+
)
|
|
158
|
+
claude_parser.add_argument(
|
|
159
|
+
"--nlp",
|
|
160
|
+
action="store_true",
|
|
161
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
162
|
+
)
|
|
163
|
+
claude_parser.add_argument(
|
|
164
|
+
"--rag",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
167
|
+
)
|
|
168
|
+
claude_parser.add_argument(
|
|
169
|
+
"--fast",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Use fast tier model (requires --rag)",
|
|
172
|
+
)
|
|
173
|
+
claude_parser.add_argument(
|
|
174
|
+
"--max",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Use maximum quality tier model (requires --rag)",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# ========================================================================
|
|
180
|
+
# CURSOR subcommand (editor setup)
|
|
181
|
+
# ========================================================================
|
|
182
|
+
cursor_parser = subparsers.add_parser(
|
|
183
|
+
"cursor",
|
|
184
|
+
help="Setup Cicada for Cursor editor",
|
|
185
|
+
description="One-command setup for Cursor with keyword extraction",
|
|
186
|
+
)
|
|
187
|
+
cursor_parser.add_argument(
|
|
188
|
+
"--nlp",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
191
|
+
)
|
|
192
|
+
cursor_parser.add_argument(
|
|
193
|
+
"--rag",
|
|
194
|
+
action="store_true",
|
|
195
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
196
|
+
)
|
|
197
|
+
cursor_parser.add_argument(
|
|
198
|
+
"--fast",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="Use fast tier model (requires --rag)",
|
|
201
|
+
)
|
|
202
|
+
cursor_parser.add_argument(
|
|
203
|
+
"--max",
|
|
204
|
+
action="store_true",
|
|
205
|
+
help="Use maximum quality tier model (requires --rag)",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# ========================================================================
|
|
209
|
+
# VS subcommand (editor setup)
|
|
210
|
+
# ========================================================================
|
|
211
|
+
vs_parser = subparsers.add_parser(
|
|
212
|
+
"vs",
|
|
213
|
+
help="Setup Cicada for VS Code editor",
|
|
214
|
+
description="One-command setup for VS Code with keyword extraction",
|
|
215
|
+
)
|
|
216
|
+
vs_parser.add_argument(
|
|
217
|
+
"--nlp",
|
|
218
|
+
action="store_true",
|
|
219
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
220
|
+
)
|
|
221
|
+
vs_parser.add_argument(
|
|
222
|
+
"--rag",
|
|
223
|
+
action="store_true",
|
|
224
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
225
|
+
)
|
|
226
|
+
vs_parser.add_argument(
|
|
227
|
+
"--fast",
|
|
228
|
+
action="store_true",
|
|
229
|
+
help="Use fast tier model (requires --rag)",
|
|
230
|
+
)
|
|
231
|
+
vs_parser.add_argument(
|
|
232
|
+
"--max",
|
|
233
|
+
action="store_true",
|
|
234
|
+
help="Use maximum quality tier model (requires --rag)",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ========================================================================
|
|
238
|
+
# INDEX subcommand
|
|
239
|
+
# ========================================================================
|
|
240
|
+
index_parser = subparsers.add_parser(
|
|
241
|
+
"index",
|
|
242
|
+
help="Index an Elixir repository to extract modules and functions",
|
|
243
|
+
description="Index current Elixir repository to extract modules and functions",
|
|
244
|
+
)
|
|
245
|
+
index_parser.add_argument(
|
|
246
|
+
"repo",
|
|
247
|
+
nargs="?",
|
|
248
|
+
default=".",
|
|
249
|
+
help="Path to the Elixir repository to index (default: current directory)",
|
|
250
|
+
)
|
|
251
|
+
index_parser.add_argument(
|
|
252
|
+
"--nlp",
|
|
253
|
+
action="store_true",
|
|
254
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
255
|
+
)
|
|
256
|
+
index_parser.add_argument(
|
|
257
|
+
"--rag",
|
|
258
|
+
action="store_true",
|
|
259
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
260
|
+
)
|
|
261
|
+
index_parser.add_argument(
|
|
262
|
+
"--fast",
|
|
263
|
+
action="store_true",
|
|
264
|
+
help="Use fast tier model (requires --rag)",
|
|
265
|
+
)
|
|
266
|
+
index_parser.add_argument(
|
|
267
|
+
"--max",
|
|
268
|
+
action="store_true",
|
|
269
|
+
help="Use maximum quality tier model (requires --rag)",
|
|
270
|
+
)
|
|
271
|
+
index_parser.add_argument(
|
|
272
|
+
"--test",
|
|
273
|
+
action="store_true",
|
|
274
|
+
help="Start interactive keyword extraction test mode",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# ========================================================================
|
|
278
|
+
# INDEX-PR subcommand
|
|
279
|
+
# ========================================================================
|
|
280
|
+
index_pr_parser = subparsers.add_parser(
|
|
281
|
+
"index-pr",
|
|
282
|
+
help="Index GitHub pull requests for fast offline lookup",
|
|
283
|
+
description="Index GitHub pull requests for fast offline lookup",
|
|
284
|
+
)
|
|
285
|
+
index_pr_parser.add_argument(
|
|
286
|
+
"repo",
|
|
287
|
+
nargs="?",
|
|
288
|
+
default=".",
|
|
289
|
+
help="Path to git repository (default: current directory)",
|
|
290
|
+
)
|
|
291
|
+
index_pr_parser.add_argument(
|
|
292
|
+
"--clean",
|
|
293
|
+
action="store_true",
|
|
294
|
+
help="Clean and rebuild the entire index from scratch (default: incremental update)",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# ========================================================================
|
|
298
|
+
# FIND-DEAD-CODE subcommand
|
|
299
|
+
# ========================================================================
|
|
300
|
+
dead_code_parser = subparsers.add_parser(
|
|
301
|
+
"find-dead-code",
|
|
302
|
+
help="Find potentially unused public functions in Elixir codebase",
|
|
303
|
+
description="Find potentially unused public functions in Elixir codebase",
|
|
304
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
305
|
+
epilog="""
|
|
306
|
+
Confidence Levels:
|
|
307
|
+
high - Zero usage, no dynamic call indicators, no behaviors/uses
|
|
308
|
+
medium - Zero usage, but module has behaviors or uses (possible callbacks)
|
|
309
|
+
low - Zero usage, but module passed as value (possible dynamic calls)
|
|
310
|
+
|
|
311
|
+
Examples:
|
|
312
|
+
cicada find-dead-code # Show high confidence candidates
|
|
313
|
+
cicada find-dead-code --min-confidence low # Show all candidates
|
|
314
|
+
cicada find-dead-code --format json # Output as JSON
|
|
315
|
+
""",
|
|
316
|
+
)
|
|
317
|
+
dead_code_parser.add_argument(
|
|
318
|
+
"--format",
|
|
319
|
+
choices=["markdown", "json"],
|
|
320
|
+
default="markdown",
|
|
321
|
+
help="Output format (default: markdown)",
|
|
322
|
+
)
|
|
323
|
+
dead_code_parser.add_argument(
|
|
324
|
+
"--min-confidence",
|
|
325
|
+
choices=["high", "medium", "low"],
|
|
326
|
+
default="high",
|
|
327
|
+
help="Minimum confidence level to show (default: high)",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# ========================================================================
|
|
331
|
+
# CLEAN subcommand
|
|
332
|
+
# ========================================================================
|
|
333
|
+
clean_parser = subparsers.add_parser(
|
|
334
|
+
"clean",
|
|
335
|
+
help="Remove Cicada configuration and indexes",
|
|
336
|
+
description="Remove Cicada configuration and indexes for current repository",
|
|
337
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
338
|
+
epilog="""
|
|
339
|
+
Examples:
|
|
340
|
+
cicada clean # Remove everything (interactive with confirmation)
|
|
341
|
+
cicada clean -f # Remove everything (skip confirmation)
|
|
342
|
+
cicada clean --index # Remove main index (index.json, hashes.json)
|
|
343
|
+
cicada clean --pr-index # Remove PR index (pr_index.json)
|
|
344
|
+
cicada clean --all # Remove ALL project storage
|
|
345
|
+
cicada clean --all -f # Remove ALL project storage (skip confirmation)
|
|
346
|
+
""",
|
|
347
|
+
)
|
|
348
|
+
clean_parser.add_argument(
|
|
349
|
+
"-f",
|
|
350
|
+
"--force",
|
|
351
|
+
action="store_true",
|
|
352
|
+
help="Skip confirmation prompt (for full clean or --all)",
|
|
353
|
+
)
|
|
354
|
+
clean_parser.add_argument(
|
|
355
|
+
"--index",
|
|
356
|
+
action="store_true",
|
|
357
|
+
help="Remove only main index files (index.json, hashes.json)",
|
|
358
|
+
)
|
|
359
|
+
clean_parser.add_argument(
|
|
360
|
+
"--pr-index",
|
|
361
|
+
action="store_true",
|
|
362
|
+
help="Remove only PR index file (pr_index.json)",
|
|
363
|
+
)
|
|
364
|
+
clean_parser.add_argument(
|
|
365
|
+
"--all",
|
|
366
|
+
action="store_true",
|
|
367
|
+
help="Remove ALL Cicada storage for all projects (~/.cicada/projects/)",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Parse arguments - now simplified with pre-processing above
|
|
371
|
+
args = parser.parse_args()
|
|
372
|
+
|
|
373
|
+
# Route to appropriate handler
|
|
374
|
+
if args.command == "install":
|
|
375
|
+
handle_install_command(args)
|
|
376
|
+
elif args.command == "server":
|
|
377
|
+
handle_server_command(args)
|
|
378
|
+
elif args.command == "claude":
|
|
379
|
+
handle_editor_setup(args, "claude")
|
|
380
|
+
elif args.command == "cursor":
|
|
381
|
+
handle_editor_setup(args, "cursor")
|
|
382
|
+
elif args.command == "vs":
|
|
383
|
+
handle_editor_setup(args, "vs")
|
|
384
|
+
elif args.command == "index":
|
|
385
|
+
handle_index(args)
|
|
386
|
+
elif args.command == "index-pr":
|
|
387
|
+
handle_index_pr(args)
|
|
388
|
+
elif args.command == "find-dead-code":
|
|
389
|
+
handle_find_dead_code(args)
|
|
390
|
+
elif args.command == "clean":
|
|
391
|
+
handle_clean(args)
|
|
392
|
+
elif args.command is None:
|
|
393
|
+
# No subcommand and no path - show help
|
|
394
|
+
parser.print_help()
|
|
395
|
+
sys.exit(1)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def handle_install_command(args):
|
|
399
|
+
"""Handle the explicit install subcommand."""
|
|
400
|
+
from cicada.mcp_entry import handle_install
|
|
401
|
+
|
|
402
|
+
handle_install(args)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def handle_server_command(args):
|
|
406
|
+
"""Handle the server subcommand (silent MCP server with optional configs)."""
|
|
407
|
+
from cicada.mcp_entry import handle_server
|
|
408
|
+
|
|
409
|
+
handle_server(args)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def handle_editor_setup(args, editor: str):
|
|
413
|
+
"""Handle editor setup subcommands (claude, cursor, vs)."""
|
|
414
|
+
from pathlib import Path
|
|
415
|
+
from typing import cast
|
|
416
|
+
|
|
417
|
+
from cicada.setup import EditorType, setup
|
|
418
|
+
|
|
419
|
+
# Validate that --fast or --max requires --rag
|
|
420
|
+
if (args.fast or args.max) and not args.rag:
|
|
421
|
+
print("Error: --fast or --max requires --rag", file=sys.stderr)
|
|
422
|
+
sys.exit(1)
|
|
423
|
+
|
|
424
|
+
# Both --nlp and --rag cannot be specified
|
|
425
|
+
if args.nlp and args.rag:
|
|
426
|
+
print("Error: Cannot specify both --nlp and --rag", file=sys.stderr)
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
|
|
429
|
+
# Use current directory as repo path
|
|
430
|
+
repo_path = Path.cwd()
|
|
431
|
+
|
|
432
|
+
# Check if it's an Elixir repository
|
|
433
|
+
if not (repo_path / "mix.exs").exists():
|
|
434
|
+
print(f"Error: {repo_path} does not appear to be an Elixir project", file=sys.stderr)
|
|
435
|
+
print("(mix.exs not found)", file=sys.stderr)
|
|
436
|
+
sys.exit(1)
|
|
437
|
+
|
|
438
|
+
# Determine keyword extraction method and tier from flags
|
|
439
|
+
keyword_method = None
|
|
440
|
+
keyword_tier = None
|
|
441
|
+
|
|
442
|
+
if args.nlp:
|
|
443
|
+
keyword_method = "lemminflect"
|
|
444
|
+
keyword_tier = "regular" # Lemminflect only has one tier
|
|
445
|
+
elif args.rag:
|
|
446
|
+
keyword_method = "bert"
|
|
447
|
+
# Determine tier from flags
|
|
448
|
+
if args.fast:
|
|
449
|
+
keyword_tier = "fast"
|
|
450
|
+
elif args.max:
|
|
451
|
+
keyword_tier = "max"
|
|
452
|
+
else:
|
|
453
|
+
keyword_tier = "regular" # Default for bert
|
|
454
|
+
|
|
455
|
+
# If no flags provided, check if index already exists
|
|
456
|
+
index_exists = False
|
|
457
|
+
if keyword_method is None:
|
|
458
|
+
from cicada.utils.storage import get_config_path, get_index_path
|
|
459
|
+
|
|
460
|
+
config_path = get_config_path(repo_path)
|
|
461
|
+
index_path = get_index_path(repo_path)
|
|
462
|
+
|
|
463
|
+
if config_path.exists() and index_path.exists():
|
|
464
|
+
# Index exists - read existing settings and mark index_exists
|
|
465
|
+
import yaml
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
with open(config_path) as f:
|
|
469
|
+
existing_config = yaml.safe_load(f)
|
|
470
|
+
keyword_method = existing_config.get("keyword_extraction", {}).get(
|
|
471
|
+
"method", "lemminflect"
|
|
472
|
+
)
|
|
473
|
+
keyword_tier = existing_config.get("keyword_extraction", {}).get(
|
|
474
|
+
"tier", "regular"
|
|
475
|
+
)
|
|
476
|
+
index_exists = True
|
|
477
|
+
except Exception:
|
|
478
|
+
# If we can't read config, proceed with defaults
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
# Run setup
|
|
482
|
+
try:
|
|
483
|
+
setup(
|
|
484
|
+
cast(EditorType, editor),
|
|
485
|
+
repo_path,
|
|
486
|
+
keyword_method=keyword_method,
|
|
487
|
+
keyword_tier=keyword_tier,
|
|
488
|
+
index_exists=index_exists,
|
|
489
|
+
)
|
|
490
|
+
except Exception as e:
|
|
491
|
+
print(f"\nError: Setup failed: {e}", file=sys.stderr)
|
|
492
|
+
sys.exit(1)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def handle_index(args):
|
|
496
|
+
"""Handle the index subcommand."""
|
|
497
|
+
from pathlib import Path
|
|
498
|
+
|
|
499
|
+
from cicada.indexer import ElixirIndexer
|
|
500
|
+
from cicada.utils.storage import get_config_path
|
|
501
|
+
from cicada.version_check import check_for_updates
|
|
502
|
+
|
|
503
|
+
# Check for updates (non-blocking, fails silently)
|
|
504
|
+
check_for_updates()
|
|
505
|
+
|
|
506
|
+
# Handle --test mode (interactive keyword extraction testing)
|
|
507
|
+
if args.test:
|
|
508
|
+
# Validate that --fast or --max requires --rag
|
|
509
|
+
if (args.fast or args.max) and not args.rag:
|
|
510
|
+
print("Error: --fast or --max requires --rag", file=sys.stderr)
|
|
511
|
+
sys.exit(1)
|
|
512
|
+
|
|
513
|
+
# Both --nlp and --rag cannot be specified
|
|
514
|
+
if args.nlp and args.rag:
|
|
515
|
+
print("Error: Cannot specify both --nlp and --rag", file=sys.stderr)
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
|
|
518
|
+
# Determine method and tier
|
|
519
|
+
if args.nlp:
|
|
520
|
+
method = "lemminflect"
|
|
521
|
+
tier = "regular"
|
|
522
|
+
elif args.rag:
|
|
523
|
+
method = "bert"
|
|
524
|
+
if args.fast:
|
|
525
|
+
tier = "fast"
|
|
526
|
+
elif args.max:
|
|
527
|
+
tier = "max"
|
|
528
|
+
else:
|
|
529
|
+
tier = "regular"
|
|
530
|
+
else:
|
|
531
|
+
# Default to lemminflect if no method specified
|
|
532
|
+
method = "lemminflect"
|
|
533
|
+
tier = "regular"
|
|
534
|
+
|
|
535
|
+
# Start interactive test mode
|
|
536
|
+
from cicada.keyword_test import run_keywords_interactive
|
|
537
|
+
|
|
538
|
+
run_keywords_interactive(method=method, tier=tier)
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
# Validate that --fast or --max requires --rag
|
|
542
|
+
if (args.fast or args.max) and not args.rag:
|
|
543
|
+
print("Error: --fast or --max requires --rag", file=sys.stderr)
|
|
544
|
+
sys.exit(1)
|
|
545
|
+
|
|
546
|
+
# Both --nlp and --rag cannot be specified
|
|
547
|
+
if args.nlp and args.rag:
|
|
548
|
+
print("Error: Cannot specify both --nlp and --rag", file=sys.stderr)
|
|
549
|
+
sys.exit(1)
|
|
550
|
+
|
|
551
|
+
# Check if config.yaml exists to determine if we need interactive setup
|
|
552
|
+
repo_path_obj = Path(args.repo).resolve()
|
|
553
|
+
config_path = get_config_path(repo_path_obj)
|
|
554
|
+
config_exists = config_path.exists()
|
|
555
|
+
|
|
556
|
+
# Use centralized storage paths
|
|
557
|
+
from cicada.utils.storage import create_storage_dir, get_index_path
|
|
558
|
+
|
|
559
|
+
storage_dir = create_storage_dir(repo_path_obj)
|
|
560
|
+
index_path = get_index_path(repo_path_obj)
|
|
561
|
+
|
|
562
|
+
# Determine keyword extraction method and tier
|
|
563
|
+
keyword_method = None
|
|
564
|
+
keyword_tier = None
|
|
565
|
+
|
|
566
|
+
# If flags provided, update config with new settings
|
|
567
|
+
if args.nlp or args.rag:
|
|
568
|
+
# User explicitly specified extraction method via flags
|
|
569
|
+
from cicada.setup import create_config_yaml
|
|
570
|
+
|
|
571
|
+
# Determine method and tier from flags
|
|
572
|
+
if args.nlp:
|
|
573
|
+
keyword_method = "lemminflect"
|
|
574
|
+
keyword_tier = "regular"
|
|
575
|
+
else: # args.rag
|
|
576
|
+
keyword_method = "bert"
|
|
577
|
+
if args.fast:
|
|
578
|
+
keyword_tier = "fast"
|
|
579
|
+
elif args.max:
|
|
580
|
+
keyword_tier = "max"
|
|
581
|
+
else:
|
|
582
|
+
keyword_tier = "regular"
|
|
583
|
+
|
|
584
|
+
# Warn if changing existing config
|
|
585
|
+
if config_exists:
|
|
586
|
+
import yaml
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
with open(config_path) as f:
|
|
590
|
+
existing_config = yaml.safe_load(f)
|
|
591
|
+
existing_method = existing_config.get("keyword_extraction", {}).get(
|
|
592
|
+
"method", "lemminflect"
|
|
593
|
+
)
|
|
594
|
+
existing_tier = existing_config.get("keyword_extraction", {}).get(
|
|
595
|
+
"tier", "regular"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Check if either method or tier has changed
|
|
599
|
+
method_changed = existing_method != keyword_method
|
|
600
|
+
tier_changed = existing_tier != keyword_tier
|
|
601
|
+
|
|
602
|
+
if method_changed or tier_changed:
|
|
603
|
+
# Build error message based on what changed
|
|
604
|
+
if method_changed and tier_changed:
|
|
605
|
+
change_desc = f"extraction method from {existing_method} to {keyword_method} and tier from {existing_tier} to {keyword_tier}"
|
|
606
|
+
elif method_changed:
|
|
607
|
+
change_desc = (
|
|
608
|
+
f"extraction method from {existing_method} to {keyword_method}"
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
change_desc = f"tier from {existing_tier} to {keyword_tier}"
|
|
612
|
+
|
|
613
|
+
print(
|
|
614
|
+
f"Error: Cannot change {change_desc}",
|
|
615
|
+
file=sys.stderr,
|
|
616
|
+
)
|
|
617
|
+
print(
|
|
618
|
+
"\nTo reindex with different settings, first run:",
|
|
619
|
+
file=sys.stderr,
|
|
620
|
+
)
|
|
621
|
+
print(" cicada clean", file=sys.stderr)
|
|
622
|
+
print("\nThen run your index command again.", file=sys.stderr)
|
|
623
|
+
sys.exit(1)
|
|
624
|
+
except Exception:
|
|
625
|
+
pass # If we can't read config, just proceed
|
|
626
|
+
|
|
627
|
+
create_config_yaml(repo_path_obj, storage_dir, keyword_method, keyword_tier)
|
|
628
|
+
config_exists = True # Config now exists
|
|
629
|
+
elif not config_exists:
|
|
630
|
+
# No flags provided AND no config exists - print help and exit
|
|
631
|
+
print("Error: No keyword extraction method specified.", file=sys.stderr)
|
|
632
|
+
print("\nYou must specify either --nlp or --rag for keyword extraction:", file=sys.stderr)
|
|
633
|
+
print(" --nlp Use NLP keyword extraction (lemminflect-based)", file=sys.stderr)
|
|
634
|
+
print(" --rag Use RAG-optimized keyword extraction (BERT-based)", file=sys.stderr)
|
|
635
|
+
print("\nRun 'cicada index --help' for more information.", file=sys.stderr)
|
|
636
|
+
sys.exit(2)
|
|
637
|
+
|
|
638
|
+
# If config exists (or was just created), indexer will read it automatically
|
|
639
|
+
indexer = ElixirIndexer(verbose=True)
|
|
640
|
+
indexer.incremental_index_repository(
|
|
641
|
+
str(repo_path_obj),
|
|
642
|
+
str(index_path), # Use centralized storage path
|
|
643
|
+
extract_keywords=True, # Always extract keywords if we have a config
|
|
644
|
+
force_full=False,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def handle_index_pr(args):
|
|
649
|
+
"""Handle the index-pr subcommand."""
|
|
650
|
+
from cicada.pr_indexer import PRIndexer
|
|
651
|
+
from cicada.utils import get_pr_index_path
|
|
652
|
+
from cicada.version_check import check_for_updates
|
|
653
|
+
|
|
654
|
+
# Check for updates (non-blocking, fails silently)
|
|
655
|
+
check_for_updates()
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
# Always use centralized storage
|
|
659
|
+
output_path = str(get_pr_index_path(args.repo))
|
|
660
|
+
|
|
661
|
+
indexer = PRIndexer(repo_path=args.repo)
|
|
662
|
+
# Incremental by default, unless --clean is specified
|
|
663
|
+
indexer.index_repository(output_path=output_path, incremental=not args.clean)
|
|
664
|
+
|
|
665
|
+
print("\n✅ Indexing complete! You can now use the MCP tools for PR history lookups.")
|
|
666
|
+
|
|
667
|
+
except KeyboardInterrupt:
|
|
668
|
+
print("\n\n⚠️ Indexing interrupted by user.")
|
|
669
|
+
print("Partial index may have been saved. Run again to continue (incremental by default).")
|
|
670
|
+
sys.exit(130) # Standard exit code for SIGINT
|
|
671
|
+
|
|
672
|
+
except Exception as e:
|
|
673
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
674
|
+
sys.exit(1)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def handle_find_dead_code(args):
|
|
678
|
+
"""Handle the find-dead-code subcommand."""
|
|
679
|
+
from cicada.dead_code_analyzer import DeadCodeAnalyzer
|
|
680
|
+
from cicada.find_dead_code import filter_by_confidence, format_json, format_markdown
|
|
681
|
+
from cicada.utils import get_index_path, load_index
|
|
682
|
+
|
|
683
|
+
# Always use centralized storage
|
|
684
|
+
index_path = get_index_path(".")
|
|
685
|
+
|
|
686
|
+
if not index_path.exists():
|
|
687
|
+
print(f"Error: Index file not found: {index_path}", file=sys.stderr)
|
|
688
|
+
print("\nRun 'cicada index' first to create the index.", file=sys.stderr)
|
|
689
|
+
sys.exit(1)
|
|
690
|
+
|
|
691
|
+
try:
|
|
692
|
+
index = load_index(index_path, raise_on_error=True)
|
|
693
|
+
except Exception as e:
|
|
694
|
+
print(f"Error loading index: {e}", file=sys.stderr)
|
|
695
|
+
sys.exit(1)
|
|
696
|
+
|
|
697
|
+
assert index is not None, "Index should not be None after successful load"
|
|
698
|
+
|
|
699
|
+
# Run analysis
|
|
700
|
+
analyzer = DeadCodeAnalyzer(index)
|
|
701
|
+
results = analyzer.analyze()
|
|
702
|
+
|
|
703
|
+
# Filter by confidence
|
|
704
|
+
results = filter_by_confidence(results, args.min_confidence)
|
|
705
|
+
|
|
706
|
+
# Format output
|
|
707
|
+
output = format_json(results) if args.format == "json" else format_markdown(results)
|
|
708
|
+
|
|
709
|
+
print(output)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def handle_clean(args):
|
|
713
|
+
"""Handle the clean subcommand."""
|
|
714
|
+
from pathlib import Path
|
|
715
|
+
|
|
716
|
+
from cicada.clean import (
|
|
717
|
+
clean_all_projects,
|
|
718
|
+
clean_index_only,
|
|
719
|
+
clean_pr_index_only,
|
|
720
|
+
clean_repository,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Handle --all flag
|
|
724
|
+
if args.all:
|
|
725
|
+
try:
|
|
726
|
+
clean_all_projects(force=args.force)
|
|
727
|
+
except Exception as e:
|
|
728
|
+
print(f"\nError: Cleanup failed: {e}", file=sys.stderr)
|
|
729
|
+
sys.exit(1)
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
# Check for conflicting flags
|
|
733
|
+
flag_count = sum([args.index, args.pr_index])
|
|
734
|
+
if flag_count > 1:
|
|
735
|
+
print("Error: Cannot specify multiple clean options.", file=sys.stderr)
|
|
736
|
+
print("Choose only one: --index, --pr-index, or -f/--force", file=sys.stderr)
|
|
737
|
+
sys.exit(1)
|
|
738
|
+
|
|
739
|
+
# Clean current directory
|
|
740
|
+
repo_path = Path.cwd()
|
|
741
|
+
|
|
742
|
+
# Run cleanup based on flags
|
|
743
|
+
try:
|
|
744
|
+
if args.index:
|
|
745
|
+
clean_index_only(repo_path)
|
|
746
|
+
elif args.pr_index:
|
|
747
|
+
clean_pr_index_only(repo_path)
|
|
748
|
+
else:
|
|
749
|
+
# No specific flag - do full clean (with or without confirmation based on --force)
|
|
750
|
+
clean_repository(repo_path, force=args.force)
|
|
751
|
+
except Exception as e:
|
|
752
|
+
print(f"\nError: Cleanup failed: {e}", file=sys.stderr)
|
|
753
|
+
sys.exit(1)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
if __name__ == "__main__":
|
|
757
|
+
main()
|