cicada-mcp 0.1.5__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 +165 -132
- 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 +110 -232
- 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 +198 -89
- 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.5.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.5.dist-info/RECORD +0 -47
- cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
cicada/mcp_entry.py
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Entry point for cicada-mcp command.
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
- With no args: Start MCP server
|
|
7
|
+
- With path arg: Start MCP server for that path
|
|
8
|
+
- cicada-mcp install: Interactive setup with editor and model selection
|
|
9
|
+
- With subcommands: Route to appropriate handler (same as cicada CLI)
|
|
10
|
+
|
|
11
|
+
This provides unified command interface for both cicada and cicada-mcp.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
"""Main entry point for cicada-mcp command."""
|
|
20
|
+
# Known subcommands
|
|
21
|
+
known_subcommands = [
|
|
22
|
+
"install",
|
|
23
|
+
"server",
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"vs",
|
|
27
|
+
"index",
|
|
28
|
+
"index-pr",
|
|
29
|
+
"find-dead-code",
|
|
30
|
+
"clean",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Handle path argument for backward compatibility (cicada-mcp <path>)
|
|
34
|
+
# If first arg is not a known subcommand and not a flag, treat it as a path
|
|
35
|
+
server_path = None
|
|
36
|
+
if (
|
|
37
|
+
len(sys.argv) > 1
|
|
38
|
+
and sys.argv[1] not in known_subcommands
|
|
39
|
+
and not sys.argv[1].startswith("-")
|
|
40
|
+
):
|
|
41
|
+
# Extract the path and remove it from sys.argv so argparse doesn't see it
|
|
42
|
+
server_path = sys.argv[1]
|
|
43
|
+
sys.argv = [sys.argv[0]] + sys.argv[2:]
|
|
44
|
+
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
prog="cicada-mcp",
|
|
47
|
+
description="Cicada MCP Server - AI-powered Elixir code analysis",
|
|
48
|
+
epilog="Run 'cicada-mcp <command> --help' for more information on a command.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Create subparsers for commands (optional to support default server mode)
|
|
52
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=False)
|
|
53
|
+
|
|
54
|
+
# ========================================================================
|
|
55
|
+
# INSTALL subcommand - Interactive setup
|
|
56
|
+
# ========================================================================
|
|
57
|
+
install_parser = subparsers.add_parser(
|
|
58
|
+
"install",
|
|
59
|
+
help="Interactive setup for Cicada",
|
|
60
|
+
description="Interactive setup with editor and model selection",
|
|
61
|
+
)
|
|
62
|
+
install_parser.add_argument(
|
|
63
|
+
"repo",
|
|
64
|
+
nargs="?",
|
|
65
|
+
default=None,
|
|
66
|
+
help="Path to Elixir repository (default: current directory)",
|
|
67
|
+
)
|
|
68
|
+
install_parser.add_argument(
|
|
69
|
+
"--claude",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Skip editor selection, use Claude Code",
|
|
72
|
+
)
|
|
73
|
+
install_parser.add_argument(
|
|
74
|
+
"--cursor",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Skip editor selection, use Cursor",
|
|
77
|
+
)
|
|
78
|
+
install_parser.add_argument(
|
|
79
|
+
"--vs",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Skip editor selection, use VS Code",
|
|
82
|
+
)
|
|
83
|
+
install_parser.add_argument(
|
|
84
|
+
"--nlp",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Skip model selection, use Lemminflect",
|
|
87
|
+
)
|
|
88
|
+
install_parser.add_argument(
|
|
89
|
+
"--rag",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Skip model selection, use BERT (default tier)",
|
|
92
|
+
)
|
|
93
|
+
install_parser.add_argument(
|
|
94
|
+
"--fast",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="Use BERT fast tier (requires --rag)",
|
|
97
|
+
)
|
|
98
|
+
install_parser.add_argument(
|
|
99
|
+
"--max",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help="Use BERT max tier (requires --rag)",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# ========================================================================
|
|
105
|
+
# SERVER subcommand - Silent MCP server
|
|
106
|
+
# ========================================================================
|
|
107
|
+
server_parser = subparsers.add_parser(
|
|
108
|
+
"server",
|
|
109
|
+
help="Start MCP server (silent mode with defaults)",
|
|
110
|
+
description="Start MCP server with auto-setup using defaults",
|
|
111
|
+
)
|
|
112
|
+
server_parser.add_argument(
|
|
113
|
+
"repo",
|
|
114
|
+
nargs="?",
|
|
115
|
+
default=None,
|
|
116
|
+
help="Path to Elixir repository (default: current directory)",
|
|
117
|
+
)
|
|
118
|
+
server_parser.add_argument(
|
|
119
|
+
"--claude",
|
|
120
|
+
action="store_true",
|
|
121
|
+
help="Create Claude Code config before starting server",
|
|
122
|
+
)
|
|
123
|
+
server_parser.add_argument(
|
|
124
|
+
"--cursor",
|
|
125
|
+
action="store_true",
|
|
126
|
+
help="Create Cursor config before starting server",
|
|
127
|
+
)
|
|
128
|
+
server_parser.add_argument(
|
|
129
|
+
"--vs",
|
|
130
|
+
action="store_true",
|
|
131
|
+
help="Create VS Code config before starting server",
|
|
132
|
+
)
|
|
133
|
+
server_parser.add_argument(
|
|
134
|
+
"--nlp",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Force Lemminflect (if reindexing needed)",
|
|
137
|
+
)
|
|
138
|
+
server_parser.add_argument(
|
|
139
|
+
"--rag",
|
|
140
|
+
action="store_true",
|
|
141
|
+
help="Force BERT (if reindexing needed)",
|
|
142
|
+
)
|
|
143
|
+
server_parser.add_argument(
|
|
144
|
+
"--fast",
|
|
145
|
+
action="store_true",
|
|
146
|
+
help="Force BERT fast tier (requires --rag)",
|
|
147
|
+
)
|
|
148
|
+
server_parser.add_argument(
|
|
149
|
+
"--max",
|
|
150
|
+
action="store_true",
|
|
151
|
+
help="Force BERT max tier (requires --rag)",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# ========================================================================
|
|
155
|
+
# CLAUDE subcommand (editor setup)
|
|
156
|
+
# ========================================================================
|
|
157
|
+
claude_parser = subparsers.add_parser(
|
|
158
|
+
"claude",
|
|
159
|
+
help="Setup Cicada for Claude Code editor",
|
|
160
|
+
description="One-command setup for Claude Code with keyword extraction",
|
|
161
|
+
)
|
|
162
|
+
claude_parser.add_argument(
|
|
163
|
+
"--nlp",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
166
|
+
)
|
|
167
|
+
claude_parser.add_argument(
|
|
168
|
+
"--rag",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
171
|
+
)
|
|
172
|
+
claude_parser.add_argument(
|
|
173
|
+
"--fast",
|
|
174
|
+
action="store_true",
|
|
175
|
+
help="Use fast tier model (requires --nlp or --rag)",
|
|
176
|
+
)
|
|
177
|
+
claude_parser.add_argument(
|
|
178
|
+
"--max",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="Use maximum quality tier model (requires --nlp or --rag)",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# ========================================================================
|
|
184
|
+
# CURSOR subcommand (editor setup)
|
|
185
|
+
# ========================================================================
|
|
186
|
+
cursor_parser = subparsers.add_parser(
|
|
187
|
+
"cursor",
|
|
188
|
+
help="Setup Cicada for Cursor editor",
|
|
189
|
+
description="One-command setup for Cursor with keyword extraction",
|
|
190
|
+
)
|
|
191
|
+
cursor_parser.add_argument(
|
|
192
|
+
"--nlp",
|
|
193
|
+
action="store_true",
|
|
194
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
195
|
+
)
|
|
196
|
+
cursor_parser.add_argument(
|
|
197
|
+
"--rag",
|
|
198
|
+
action="store_true",
|
|
199
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
200
|
+
)
|
|
201
|
+
cursor_parser.add_argument(
|
|
202
|
+
"--fast",
|
|
203
|
+
action="store_true",
|
|
204
|
+
help="Use fast tier model (requires --nlp or --rag)",
|
|
205
|
+
)
|
|
206
|
+
cursor_parser.add_argument(
|
|
207
|
+
"--max",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="Use maximum quality tier model (requires --nlp or --rag)",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# ========================================================================
|
|
213
|
+
# VS subcommand (editor setup)
|
|
214
|
+
# ========================================================================
|
|
215
|
+
vs_parser = subparsers.add_parser(
|
|
216
|
+
"vs",
|
|
217
|
+
help="Setup Cicada for VS Code editor",
|
|
218
|
+
description="One-command setup for VS Code with keyword extraction",
|
|
219
|
+
)
|
|
220
|
+
vs_parser.add_argument(
|
|
221
|
+
"--nlp",
|
|
222
|
+
action="store_true",
|
|
223
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
224
|
+
)
|
|
225
|
+
vs_parser.add_argument(
|
|
226
|
+
"--rag",
|
|
227
|
+
action="store_true",
|
|
228
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
229
|
+
)
|
|
230
|
+
vs_parser.add_argument(
|
|
231
|
+
"--fast",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help="Use fast tier model (requires --nlp or --rag)",
|
|
234
|
+
)
|
|
235
|
+
vs_parser.add_argument(
|
|
236
|
+
"--max",
|
|
237
|
+
action="store_true",
|
|
238
|
+
help="Use maximum quality tier model (requires --nlp or --rag)",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# ========================================================================
|
|
242
|
+
# INDEX subcommand
|
|
243
|
+
# ========================================================================
|
|
244
|
+
index_parser = subparsers.add_parser(
|
|
245
|
+
"index",
|
|
246
|
+
help="Index an Elixir repository to extract modules and functions",
|
|
247
|
+
description="Index current Elixir repository to extract modules and functions",
|
|
248
|
+
)
|
|
249
|
+
index_parser.add_argument(
|
|
250
|
+
"repo",
|
|
251
|
+
nargs="?",
|
|
252
|
+
default=".",
|
|
253
|
+
help="Path to the Elixir repository to index (default: current directory)",
|
|
254
|
+
)
|
|
255
|
+
index_parser.add_argument(
|
|
256
|
+
"--nlp",
|
|
257
|
+
action="store_true",
|
|
258
|
+
help="Use NLP keyword extraction (lemminflect-based)",
|
|
259
|
+
)
|
|
260
|
+
index_parser.add_argument(
|
|
261
|
+
"--rag",
|
|
262
|
+
action="store_true",
|
|
263
|
+
help="Use RAG-optimized keyword extraction (BERT-based embeddings)",
|
|
264
|
+
)
|
|
265
|
+
index_parser.add_argument(
|
|
266
|
+
"--fast",
|
|
267
|
+
action="store_true",
|
|
268
|
+
help="Use fast tier model (requires --nlp or --rag)",
|
|
269
|
+
)
|
|
270
|
+
index_parser.add_argument(
|
|
271
|
+
"--max",
|
|
272
|
+
action="store_true",
|
|
273
|
+
help="Use maximum quality tier model (requires --nlp or --rag)",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# ========================================================================
|
|
277
|
+
# INDEX-PR subcommand
|
|
278
|
+
# ========================================================================
|
|
279
|
+
index_pr_parser = subparsers.add_parser(
|
|
280
|
+
"index-pr",
|
|
281
|
+
help="Index GitHub pull requests for fast offline lookup",
|
|
282
|
+
description="Index GitHub pull requests for fast offline lookup",
|
|
283
|
+
)
|
|
284
|
+
index_pr_parser.add_argument(
|
|
285
|
+
"repo",
|
|
286
|
+
nargs="?",
|
|
287
|
+
default=".",
|
|
288
|
+
help="Path to git repository (default: current directory)",
|
|
289
|
+
)
|
|
290
|
+
index_pr_parser.add_argument(
|
|
291
|
+
"--clean",
|
|
292
|
+
action="store_true",
|
|
293
|
+
help="Clean and rebuild the entire index from scratch (default: incremental update)",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# ========================================================================
|
|
297
|
+
# FIND-DEAD-CODE subcommand
|
|
298
|
+
# ========================================================================
|
|
299
|
+
dead_code_parser = subparsers.add_parser(
|
|
300
|
+
"find-dead-code",
|
|
301
|
+
help="Find potentially unused public functions in Elixir codebase",
|
|
302
|
+
description="Find potentially unused public functions in Elixir codebase",
|
|
303
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
304
|
+
epilog="""
|
|
305
|
+
Confidence Levels:
|
|
306
|
+
high - Zero usage, no dynamic call indicators, no behaviors/uses
|
|
307
|
+
medium - Zero usage, but module has behaviors or uses (possible callbacks)
|
|
308
|
+
low - Zero usage, but module passed as value (possible dynamic calls)
|
|
309
|
+
|
|
310
|
+
Examples:
|
|
311
|
+
cicada-mcp find-dead-code # Show high confidence candidates
|
|
312
|
+
cicada-mcp find-dead-code --min-confidence low # Show all candidates
|
|
313
|
+
cicada-mcp find-dead-code --format json # Output as JSON
|
|
314
|
+
""",
|
|
315
|
+
)
|
|
316
|
+
dead_code_parser.add_argument(
|
|
317
|
+
"--index",
|
|
318
|
+
default=None,
|
|
319
|
+
help="Path to index file (default: uses current directory's centralized index)",
|
|
320
|
+
)
|
|
321
|
+
dead_code_parser.add_argument(
|
|
322
|
+
"--format",
|
|
323
|
+
choices=["markdown", "json"],
|
|
324
|
+
default="markdown",
|
|
325
|
+
help="Output format (default: markdown)",
|
|
326
|
+
)
|
|
327
|
+
dead_code_parser.add_argument(
|
|
328
|
+
"--min-confidence",
|
|
329
|
+
choices=["high", "medium", "low"],
|
|
330
|
+
default="high",
|
|
331
|
+
help="Minimum confidence level to show (default: high)",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# ========================================================================
|
|
335
|
+
# CLEAN subcommand
|
|
336
|
+
# ========================================================================
|
|
337
|
+
clean_parser = subparsers.add_parser(
|
|
338
|
+
"clean",
|
|
339
|
+
help="Remove Cicada configuration and indexes",
|
|
340
|
+
description="Remove Cicada configuration and indexes for current repository",
|
|
341
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
342
|
+
epilog="""
|
|
343
|
+
Examples:
|
|
344
|
+
cicada-mcp clean # Remove everything (interactive with confirmation)
|
|
345
|
+
cicada-mcp clean -f # Remove everything (skip confirmation)
|
|
346
|
+
cicada-mcp clean --index # Remove main index (index.json, hashes.json)
|
|
347
|
+
cicada-mcp clean --pr-index # Remove PR index (pr_index.json)
|
|
348
|
+
cicada-mcp clean --all # Remove ALL project storage
|
|
349
|
+
cicada-mcp clean --all -f # Remove ALL project storage (skip confirmation)
|
|
350
|
+
""",
|
|
351
|
+
)
|
|
352
|
+
clean_parser.add_argument(
|
|
353
|
+
"-f",
|
|
354
|
+
"--force",
|
|
355
|
+
action="store_true",
|
|
356
|
+
help="Skip confirmation prompt (for full clean or --all)",
|
|
357
|
+
)
|
|
358
|
+
clean_parser.add_argument(
|
|
359
|
+
"--index",
|
|
360
|
+
action="store_true",
|
|
361
|
+
help="Remove only main index files (index.json, hashes.json)",
|
|
362
|
+
)
|
|
363
|
+
clean_parser.add_argument(
|
|
364
|
+
"--pr-index",
|
|
365
|
+
action="store_true",
|
|
366
|
+
help="Remove only PR index file (pr_index.json)",
|
|
367
|
+
)
|
|
368
|
+
clean_parser.add_argument(
|
|
369
|
+
"--all",
|
|
370
|
+
action="store_true",
|
|
371
|
+
help="Remove ALL Cicada storage for all projects (~/.cicada/projects/)",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Parse arguments
|
|
375
|
+
args = parser.parse_args()
|
|
376
|
+
|
|
377
|
+
# Store the server path for default handler
|
|
378
|
+
args._server_path = server_path
|
|
379
|
+
|
|
380
|
+
# Route to appropriate handler
|
|
381
|
+
if args.command == "install":
|
|
382
|
+
handle_install(args)
|
|
383
|
+
elif args.command == "server":
|
|
384
|
+
handle_server(args)
|
|
385
|
+
elif args.command == "claude":
|
|
386
|
+
from cicada.cli import handle_editor_setup
|
|
387
|
+
|
|
388
|
+
handle_editor_setup(args, "claude")
|
|
389
|
+
elif args.command == "cursor":
|
|
390
|
+
from cicada.cli import handle_editor_setup
|
|
391
|
+
|
|
392
|
+
handle_editor_setup(args, "cursor")
|
|
393
|
+
elif args.command == "vs":
|
|
394
|
+
from cicada.cli import handle_editor_setup
|
|
395
|
+
|
|
396
|
+
handle_editor_setup(args, "vs")
|
|
397
|
+
elif args.command == "index":
|
|
398
|
+
from cicada.cli import handle_index
|
|
399
|
+
|
|
400
|
+
handle_index(args)
|
|
401
|
+
elif args.command == "index-pr":
|
|
402
|
+
from cicada.cli import handle_index_pr
|
|
403
|
+
|
|
404
|
+
handle_index_pr(args)
|
|
405
|
+
elif args.command == "find-dead-code":
|
|
406
|
+
from cicada.cli import handle_find_dead_code
|
|
407
|
+
|
|
408
|
+
handle_find_dead_code(args)
|
|
409
|
+
elif args.command == "clean":
|
|
410
|
+
from cicada.cli import handle_clean
|
|
411
|
+
|
|
412
|
+
handle_clean(args)
|
|
413
|
+
else:
|
|
414
|
+
# No subcommand - start server
|
|
415
|
+
handle_default_server(args)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def handle_default_server(args):
|
|
419
|
+
"""
|
|
420
|
+
Handle default behavior when called with no subcommand.
|
|
421
|
+
Starts MCP server silently.
|
|
422
|
+
"""
|
|
423
|
+
import asyncio
|
|
424
|
+
import os
|
|
425
|
+
from pathlib import Path
|
|
426
|
+
|
|
427
|
+
# Check if a path was provided (backward compatibility: cicada-mcp <path>)
|
|
428
|
+
if hasattr(args, "_server_path") and args._server_path:
|
|
429
|
+
repo_path = Path(args._server_path).resolve()
|
|
430
|
+
os.environ["CICADA_REPO_PATH"] = str(repo_path)
|
|
431
|
+
|
|
432
|
+
# Import and run MCP server
|
|
433
|
+
from cicada.mcp_server import async_main
|
|
434
|
+
|
|
435
|
+
asyncio.run(async_main())
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def handle_install(args):
|
|
439
|
+
"""
|
|
440
|
+
Handle the install subcommand (interactive setup).
|
|
441
|
+
|
|
442
|
+
Behavior:
|
|
443
|
+
- INTERACTIVE: shows prompts and menus
|
|
444
|
+
- Can skip prompts with flags (--claude, --cursor, --vs, --nlp, --rag)
|
|
445
|
+
- Creates editor config and indexes repository
|
|
446
|
+
"""
|
|
447
|
+
from pathlib import Path
|
|
448
|
+
|
|
449
|
+
from cicada.interactive_setup import show_first_time_setup
|
|
450
|
+
from cicada.setup import EditorType, setup
|
|
451
|
+
from cicada.utils import get_config_path, get_index_path
|
|
452
|
+
|
|
453
|
+
# Determine repository path
|
|
454
|
+
repo_path = Path(args.repo).resolve() if args.repo else Path.cwd().resolve()
|
|
455
|
+
|
|
456
|
+
# Validate it's an Elixir project
|
|
457
|
+
if not (repo_path / "mix.exs").exists():
|
|
458
|
+
print(f"Error: {repo_path} does not appear to be an Elixir project", file=sys.stderr)
|
|
459
|
+
print("(mix.exs not found)", file=sys.stderr)
|
|
460
|
+
sys.exit(1)
|
|
461
|
+
|
|
462
|
+
# Validate flag combinations
|
|
463
|
+
if (args.fast or args.max) and not args.rag:
|
|
464
|
+
print("Error: --fast or --max requires --rag", file=sys.stderr)
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
|
|
467
|
+
if args.nlp and args.rag:
|
|
468
|
+
print("Error: Cannot specify both --nlp and --rag", file=sys.stderr)
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
|
|
471
|
+
# Count editor flags
|
|
472
|
+
editor_flags = [args.claude, args.cursor, args.vs]
|
|
473
|
+
editor_count = sum(editor_flags)
|
|
474
|
+
|
|
475
|
+
if editor_count > 1:
|
|
476
|
+
print("Error: Can only specify one editor flag for install command", file=sys.stderr)
|
|
477
|
+
sys.exit(1)
|
|
478
|
+
|
|
479
|
+
# Determine editor from flags
|
|
480
|
+
editor: EditorType | None = None
|
|
481
|
+
if args.claude:
|
|
482
|
+
editor = "claude"
|
|
483
|
+
elif args.cursor:
|
|
484
|
+
editor = "cursor"
|
|
485
|
+
elif args.vs:
|
|
486
|
+
editor = "vs"
|
|
487
|
+
|
|
488
|
+
# Determine keyword method and tier from flags
|
|
489
|
+
keyword_method = None
|
|
490
|
+
keyword_tier = None
|
|
491
|
+
|
|
492
|
+
if args.nlp:
|
|
493
|
+
keyword_method = "lemminflect"
|
|
494
|
+
keyword_tier = "regular"
|
|
495
|
+
elif args.rag:
|
|
496
|
+
keyword_method = "bert"
|
|
497
|
+
if args.fast:
|
|
498
|
+
keyword_tier = "fast"
|
|
499
|
+
elif args.max:
|
|
500
|
+
keyword_tier = "max"
|
|
501
|
+
else:
|
|
502
|
+
keyword_tier = "regular"
|
|
503
|
+
|
|
504
|
+
# Check if index already exists
|
|
505
|
+
config_path = get_config_path(repo_path)
|
|
506
|
+
index_path = get_index_path(repo_path)
|
|
507
|
+
index_exists = config_path.exists() and index_path.exists()
|
|
508
|
+
|
|
509
|
+
# If no flags provided, use full interactive setup
|
|
510
|
+
if editor is None and keyword_method is None:
|
|
511
|
+
from cicada.interactive_setup import show_full_interactive_setup
|
|
512
|
+
|
|
513
|
+
show_full_interactive_setup(repo_path)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# If only model flags provided (no editor), prompt for editor
|
|
517
|
+
if editor is None:
|
|
518
|
+
# Show editor selection menu
|
|
519
|
+
from simple_term_menu import TerminalMenu
|
|
520
|
+
|
|
521
|
+
print("Select editor to configure:")
|
|
522
|
+
print()
|
|
523
|
+
editor_options = [
|
|
524
|
+
"Claude Code (Claude AI assistant)",
|
|
525
|
+
"Cursor (AI-powered code editor)",
|
|
526
|
+
"VS Code (Visual Studio Code)",
|
|
527
|
+
]
|
|
528
|
+
editor_menu = TerminalMenu(editor_options, title="Choose your editor:")
|
|
529
|
+
menu_idx = editor_menu.show()
|
|
530
|
+
|
|
531
|
+
if menu_idx is None:
|
|
532
|
+
print("\nSetup cancelled.")
|
|
533
|
+
sys.exit(0)
|
|
534
|
+
|
|
535
|
+
# Map menu index to editor type (menu_idx is guaranteed to be int here)
|
|
536
|
+
assert isinstance(menu_idx, int), "menu_idx must be an integer"
|
|
537
|
+
editor_map: tuple[EditorType, EditorType, EditorType] = ("claude", "cursor", "vs")
|
|
538
|
+
editor = editor_map[menu_idx]
|
|
539
|
+
|
|
540
|
+
# If only editor flag provided (no model), prompt for model (unless index exists)
|
|
541
|
+
if keyword_method is None and not index_exists:
|
|
542
|
+
keyword_method, keyword_tier = show_first_time_setup()
|
|
543
|
+
|
|
544
|
+
# If index exists but no model flags, use existing settings
|
|
545
|
+
if keyword_method is None and index_exists:
|
|
546
|
+
import yaml
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
with open(config_path) as f:
|
|
550
|
+
existing_config = yaml.safe_load(f)
|
|
551
|
+
keyword_method = existing_config.get("keyword_extraction", {}).get(
|
|
552
|
+
"method", "lemminflect"
|
|
553
|
+
)
|
|
554
|
+
keyword_tier = existing_config.get("keyword_extraction", {}).get("tier", "regular")
|
|
555
|
+
except Exception:
|
|
556
|
+
# If we can't read config, use defaults
|
|
557
|
+
keyword_method = "lemminflect"
|
|
558
|
+
keyword_tier = "regular"
|
|
559
|
+
|
|
560
|
+
# Run setup
|
|
561
|
+
try:
|
|
562
|
+
setup(
|
|
563
|
+
editor,
|
|
564
|
+
repo_path,
|
|
565
|
+
keyword_method=keyword_method,
|
|
566
|
+
keyword_tier=keyword_tier,
|
|
567
|
+
index_exists=index_exists,
|
|
568
|
+
)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"\nError: Setup failed: {e}", file=sys.stderr)
|
|
571
|
+
sys.exit(1)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def handle_server(args):
|
|
575
|
+
"""
|
|
576
|
+
Handle the server subcommand (silent MCP server with optional configs).
|
|
577
|
+
|
|
578
|
+
Behavior:
|
|
579
|
+
- SILENT: no prompts, no interactive menus
|
|
580
|
+
- Auto-setup if needed (uses default model: lemminflect)
|
|
581
|
+
- Creates editor configs if flags provided (--claude, --cursor, --vs)
|
|
582
|
+
- Starts MCP server on stdio
|
|
583
|
+
"""
|
|
584
|
+
import asyncio
|
|
585
|
+
import os
|
|
586
|
+
from pathlib import Path
|
|
587
|
+
|
|
588
|
+
from cicada.setup import (
|
|
589
|
+
EditorType,
|
|
590
|
+
create_config_yaml,
|
|
591
|
+
index_repository,
|
|
592
|
+
setup_multiple_editors,
|
|
593
|
+
)
|
|
594
|
+
from cicada.utils import create_storage_dir, get_config_path, get_index_path
|
|
595
|
+
|
|
596
|
+
# Determine repository path
|
|
597
|
+
repo_path = Path(args.repo).resolve() if args.repo else Path.cwd().resolve()
|
|
598
|
+
|
|
599
|
+
# Validate it's an Elixir project
|
|
600
|
+
if not (repo_path / "mix.exs").exists():
|
|
601
|
+
print(
|
|
602
|
+
f"Error: {repo_path} does not appear to be an Elixir project (mix.exs not found)",
|
|
603
|
+
file=sys.stderr,
|
|
604
|
+
)
|
|
605
|
+
sys.exit(1)
|
|
606
|
+
|
|
607
|
+
# Validate flag combinations
|
|
608
|
+
if (args.fast or args.max) and not args.rag:
|
|
609
|
+
print("Error: --fast or --max requires --rag", file=sys.stderr)
|
|
610
|
+
sys.exit(1)
|
|
611
|
+
|
|
612
|
+
if args.nlp and args.rag:
|
|
613
|
+
print("Error: Cannot specify both --nlp and --rag", file=sys.stderr)
|
|
614
|
+
sys.exit(1)
|
|
615
|
+
|
|
616
|
+
# Create storage directory
|
|
617
|
+
storage_dir = create_storage_dir(repo_path)
|
|
618
|
+
|
|
619
|
+
# Determine keyword extraction method and tier
|
|
620
|
+
keyword_method = None
|
|
621
|
+
keyword_tier = None
|
|
622
|
+
|
|
623
|
+
if args.nlp:
|
|
624
|
+
keyword_method = "lemminflect"
|
|
625
|
+
keyword_tier = "regular"
|
|
626
|
+
elif args.rag:
|
|
627
|
+
keyword_method = "bert"
|
|
628
|
+
if args.fast:
|
|
629
|
+
keyword_tier = "fast"
|
|
630
|
+
elif args.max:
|
|
631
|
+
keyword_tier = "max"
|
|
632
|
+
else:
|
|
633
|
+
keyword_tier = "regular"
|
|
634
|
+
|
|
635
|
+
# Check if setup is needed
|
|
636
|
+
config_path = get_config_path(repo_path)
|
|
637
|
+
index_path = get_index_path(repo_path)
|
|
638
|
+
needs_setup = not (config_path.exists() and index_path.exists())
|
|
639
|
+
|
|
640
|
+
if needs_setup:
|
|
641
|
+
# Silent setup with defaults
|
|
642
|
+
# If no method specified, default to lemminflect (fastest, no downloads)
|
|
643
|
+
if keyword_method is None:
|
|
644
|
+
keyword_method = "lemminflect"
|
|
645
|
+
keyword_tier = "regular"
|
|
646
|
+
|
|
647
|
+
# Create config.yaml (silent)
|
|
648
|
+
create_config_yaml(repo_path, storage_dir, keyword_method, keyword_tier, verbose=False)
|
|
649
|
+
|
|
650
|
+
# Index repository (silent)
|
|
651
|
+
try:
|
|
652
|
+
index_repository(repo_path, force_full=False, verbose=False)
|
|
653
|
+
except Exception as e:
|
|
654
|
+
print(f"Error during indexing: {e}", file=sys.stderr)
|
|
655
|
+
sys.exit(1)
|
|
656
|
+
|
|
657
|
+
# Create editor configs if flags provided
|
|
658
|
+
editors_to_configure: list[EditorType] = []
|
|
659
|
+
if args.claude:
|
|
660
|
+
editors_to_configure.append("claude")
|
|
661
|
+
if args.cursor:
|
|
662
|
+
editors_to_configure.append("cursor")
|
|
663
|
+
if args.vs:
|
|
664
|
+
editors_to_configure.append("vs")
|
|
665
|
+
|
|
666
|
+
if editors_to_configure:
|
|
667
|
+
try:
|
|
668
|
+
setup_multiple_editors(editors_to_configure, repo_path, storage_dir, verbose=False)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
print(f"Error creating editor configs: {e}", file=sys.stderr)
|
|
671
|
+
sys.exit(1)
|
|
672
|
+
|
|
673
|
+
# Set environment variable for MCP server
|
|
674
|
+
os.environ["CICADA_REPO_PATH"] = str(repo_path)
|
|
675
|
+
|
|
676
|
+
# Start MCP server (silent)
|
|
677
|
+
from cicada.mcp_server import async_main
|
|
678
|
+
|
|
679
|
+
asyncio.run(async_main())
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
if __name__ == "__main__":
|
|
683
|
+
main()
|