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.
Files changed (53) hide show
  1. cicada/ascii_art.py +60 -0
  2. cicada/clean.py +195 -60
  3. cicada/cli.py +757 -0
  4. cicada/colors.py +27 -0
  5. cicada/command_logger.py +14 -16
  6. cicada/dead_code_analyzer.py +12 -19
  7. cicada/extractors/__init__.py +6 -6
  8. cicada/extractors/base.py +3 -3
  9. cicada/extractors/call.py +11 -15
  10. cicada/extractors/dependency.py +39 -51
  11. cicada/extractors/doc.py +8 -9
  12. cicada/extractors/function.py +12 -24
  13. cicada/extractors/module.py +11 -15
  14. cicada/extractors/spec.py +8 -12
  15. cicada/find_dead_code.py +15 -39
  16. cicada/formatter.py +37 -91
  17. cicada/git_helper.py +22 -34
  18. cicada/indexer.py +165 -132
  19. cicada/interactive_setup.py +490 -0
  20. cicada/keybert_extractor.py +286 -0
  21. cicada/keyword_search.py +22 -30
  22. cicada/keyword_test.py +127 -0
  23. cicada/lightweight_keyword_extractor.py +5 -13
  24. cicada/mcp_entry.py +683 -0
  25. cicada/mcp_server.py +110 -232
  26. cicada/parser.py +9 -9
  27. cicada/pr_finder.py +15 -19
  28. cicada/pr_indexer/__init__.py +3 -3
  29. cicada/pr_indexer/cli.py +4 -9
  30. cicada/pr_indexer/github_api_client.py +22 -37
  31. cicada/pr_indexer/indexer.py +17 -29
  32. cicada/pr_indexer/line_mapper.py +8 -12
  33. cicada/pr_indexer/pr_index_builder.py +22 -34
  34. cicada/setup.py +198 -89
  35. cicada/utils/__init__.py +9 -9
  36. cicada/utils/call_site_formatter.py +4 -6
  37. cicada/utils/function_grouper.py +4 -4
  38. cicada/utils/hash_utils.py +12 -15
  39. cicada/utils/index_utils.py +15 -15
  40. cicada/utils/path_utils.py +24 -29
  41. cicada/utils/signature_builder.py +3 -3
  42. cicada/utils/subprocess_runner.py +17 -19
  43. cicada/utils/text_utils.py +1 -2
  44. cicada/version_check.py +2 -5
  45. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
  46. cicada_mcp-0.2.0.dist-info/RECORD +53 -0
  47. cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  48. cicada/install.py +0 -741
  49. cicada_mcp-0.1.5.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {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()