cicada-mcp 0.1.4__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.

Potentially problematic release.


This version of cicada-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. cicada/__init__.py +30 -0
  2. cicada/clean.py +297 -0
  3. cicada/command_logger.py +293 -0
  4. cicada/dead_code_analyzer.py +282 -0
  5. cicada/extractors/__init__.py +36 -0
  6. cicada/extractors/base.py +66 -0
  7. cicada/extractors/call.py +176 -0
  8. cicada/extractors/dependency.py +361 -0
  9. cicada/extractors/doc.py +179 -0
  10. cicada/extractors/function.py +246 -0
  11. cicada/extractors/module.py +123 -0
  12. cicada/extractors/spec.py +151 -0
  13. cicada/find_dead_code.py +270 -0
  14. cicada/formatter.py +918 -0
  15. cicada/git_helper.py +646 -0
  16. cicada/indexer.py +629 -0
  17. cicada/install.py +724 -0
  18. cicada/keyword_extractor.py +364 -0
  19. cicada/keyword_search.py +553 -0
  20. cicada/lightweight_keyword_extractor.py +298 -0
  21. cicada/mcp_server.py +1559 -0
  22. cicada/mcp_tools.py +291 -0
  23. cicada/parser.py +124 -0
  24. cicada/pr_finder.py +435 -0
  25. cicada/pr_indexer/__init__.py +20 -0
  26. cicada/pr_indexer/cli.py +62 -0
  27. cicada/pr_indexer/github_api_client.py +431 -0
  28. cicada/pr_indexer/indexer.py +297 -0
  29. cicada/pr_indexer/line_mapper.py +209 -0
  30. cicada/pr_indexer/pr_index_builder.py +253 -0
  31. cicada/setup.py +339 -0
  32. cicada/utils/__init__.py +52 -0
  33. cicada/utils/call_site_formatter.py +95 -0
  34. cicada/utils/function_grouper.py +57 -0
  35. cicada/utils/hash_utils.py +173 -0
  36. cicada/utils/index_utils.py +290 -0
  37. cicada/utils/path_utils.py +240 -0
  38. cicada/utils/signature_builder.py +106 -0
  39. cicada/utils/storage.py +111 -0
  40. cicada/utils/subprocess_runner.py +182 -0
  41. cicada/utils/text_utils.py +90 -0
  42. cicada/version_check.py +116 -0
  43. cicada_mcp-0.1.4.dist-info/METADATA +619 -0
  44. cicada_mcp-0.1.4.dist-info/RECORD +48 -0
  45. cicada_mcp-0.1.4.dist-info/WHEEL +5 -0
  46. cicada_mcp-0.1.4.dist-info/entry_points.txt +8 -0
  47. cicada_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. cicada_mcp-0.1.4.dist-info/top_level.txt +1 -0
cicada/install.py ADDED
@@ -0,0 +1,724 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Cicada One-Command Setup Script.
4
+
5
+ Downloads the tool, indexes the repository, and creates .mcp.json configuration.
6
+ """
7
+
8
+ import argparse
9
+ import importlib
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ def run_command(cmd, cwd=None, check=True):
17
+ """Run a shell command and return the result."""
18
+ try:
19
+ result = subprocess.run(
20
+ cmd, shell=True, check=check, cwd=cwd, capture_output=True, text=True
21
+ )
22
+ return result
23
+ except subprocess.CalledProcessError as e:
24
+ print(f"Error running command: {cmd}", file=sys.stderr)
25
+ print(f"Error: {e.stderr}", file=sys.stderr)
26
+ raise
27
+
28
+
29
+ def check_python():
30
+ """Check if Python 3.10+ is available."""
31
+ version = sys.version_info
32
+ if version.major < 3 or (version.major == 3 and version.minor < 10):
33
+ print(
34
+ f"Error: Python 3.10+ required. Current: {version.major}.{version.minor}",
35
+ file=sys.stderr,
36
+ )
37
+ sys.exit(1)
38
+ print(f"✓ Python {version.major}.{version.minor} detected")
39
+
40
+
41
+ def install_cicada(target_dir, github_url=None):
42
+ """
43
+ Install cicada from GitHub or use existing installation.
44
+
45
+ Args:
46
+ target_dir: Directory where cicada will be installed
47
+ github_url: GitHub URL to clone from (optional)
48
+
49
+ Returns:
50
+ Tuple of (Path to the cicada installation, bool indicating if already installed)
51
+ """
52
+ target_path = Path(target_dir).resolve()
53
+
54
+ # Check if we're running from an installed package (pip/uvx)
55
+ # In this case, the cicada module is already available
56
+ try:
57
+ mcp_server_module = importlib.import_module("cicada.mcp_server")
58
+ # Get the site-packages or installation directory
59
+ if mcp_server_module.__file__ is None:
60
+ raise ImportError("Could not determine module path")
61
+ package_path = Path(mcp_server_module.__file__).parent.parent
62
+ print(f"✓ Using installed cicada package")
63
+ return package_path, True # Already installed
64
+ except ImportError:
65
+ pass
66
+
67
+ # If we're already in the cicada directory, use it
68
+ current_dir = Path.cwd()
69
+ if (current_dir / "cicada" / "mcp_server.py").exists():
70
+ print(f"✓ Using existing cicada installation at {current_dir}")
71
+ return current_dir, False
72
+
73
+ # Check if target directory already has cicada
74
+ if (target_path / "cicada" / "mcp_server.py").exists():
75
+ print(f"✓ Using existing cicada installation at {target_path}")
76
+ return target_path, False
77
+
78
+ # Download from GitHub
79
+ if github_url:
80
+ print(f"Downloading cicada from {github_url}...")
81
+ target_path.parent.mkdir(parents=True, exist_ok=True)
82
+ _ = run_command(f"git clone {github_url} {target_path}")
83
+ print(f"✓ Downloaded cicada to {target_path}")
84
+ else:
85
+ print("Error: cicada not found and no GitHub URL provided", file=sys.stderr)
86
+ print(
87
+ "Hint: Run with --github-url https://github.com/wende/cicada.git",
88
+ file=sys.stderr,
89
+ )
90
+ sys.exit(1)
91
+
92
+ return target_path, False
93
+
94
+
95
+ def check_uv_available():
96
+ """Check if uv is available on the system."""
97
+ try:
98
+ result = run_command("uv --version", check=False)
99
+ return result.returncode == 0
100
+ except Exception:
101
+ return False
102
+
103
+
104
+ def install_dependencies_uv(cicada_dir):
105
+ """Install Python dependencies using uv (fast!)."""
106
+ print("Installing dependencies with uv...")
107
+
108
+ # Use uv to sync dependencies
109
+ # uv will automatically create a venv and install everything
110
+ _ = run_command(f"uv sync", cwd=cicada_dir)
111
+
112
+ # Find the python binary uv created
113
+ venv_path = cicada_dir / ".venv"
114
+ python_bin = venv_path / "bin" / "python"
115
+
116
+ if not python_bin.exists():
117
+ # Try alternative venv location
118
+ venv_path = cicada_dir / "venv"
119
+ python_bin = venv_path / "bin" / "python"
120
+
121
+ print("✓ Dependencies installed with uv")
122
+ return python_bin
123
+
124
+
125
+ def install_dependencies_pip(cicada_dir):
126
+ """Install Python dependencies using traditional pip (legacy method)."""
127
+ print("Installing dependencies with pip (legacy method)...")
128
+
129
+ # Check if venv exists
130
+ venv_path = cicada_dir / "venv"
131
+ python_bin = venv_path / "bin" / "python"
132
+
133
+ if not venv_path.exists():
134
+ print("Creating virtual environment...")
135
+ _ = run_command(f"python -m venv {venv_path}")
136
+
137
+ # Install dependencies
138
+ requirements_file = cicada_dir / "requirements.txt"
139
+ if requirements_file.exists():
140
+ _ = run_command(f"{python_bin} -m pip install -r {requirements_file}")
141
+
142
+ # Install package in editable mode
143
+ _ = run_command(f"{python_bin} -m pip install -e {cicada_dir}")
144
+
145
+ print("✓ Dependencies installed with pip")
146
+ return python_bin
147
+
148
+
149
+ def install_dependencies(cicada_dir, use_uv=None):
150
+ """
151
+ Install Python dependencies for cicada.
152
+
153
+ Args:
154
+ cicada_dir: Directory where cicada is installed
155
+ use_uv: If True, use uv; if False, use pip; if None, auto-detect
156
+
157
+ Returns:
158
+ Path to python binary
159
+ """
160
+ # Auto-detect uv if not specified (uv is preferred)
161
+ if use_uv is None:
162
+ use_uv = check_uv_available()
163
+ if use_uv:
164
+ print("✓ Detected uv - using it for faster installation (recommended)")
165
+ else:
166
+ print("⚠ uv not available - falling back to pip (slower)")
167
+
168
+ if use_uv:
169
+ return install_dependencies_uv(cicada_dir)
170
+ else:
171
+ return install_dependencies_pip(cicada_dir)
172
+
173
+
174
+ def index_repository(
175
+ cicada_dir, python_bin, repo_path, fetch_pr_info=False, spacy_model="small"
176
+ ):
177
+ """Index the Elixir repository."""
178
+ print(f"Indexing repository at {repo_path}...")
179
+
180
+ repo_path = Path(repo_path).resolve()
181
+ output_path = repo_path / ".cicada" / "index.json"
182
+
183
+ # Check if .cicada directory exists (first run detection)
184
+ is_first_run = not output_path.parent.exists()
185
+
186
+ # Create .cicada directory
187
+ output_path.parent.mkdir(parents=True, exist_ok=True)
188
+
189
+ # On first run, add .cicada/ to .gitignore if it exists
190
+ if is_first_run:
191
+ from cicada.utils.path_utils import ensure_gitignore_has_cicada
192
+
193
+ if ensure_gitignore_has_cicada(repo_path):
194
+ print("✓ Added .cicada/ to .gitignore")
195
+
196
+ # Run indexer
197
+ indexer_script = cicada_dir / "cicada" / "indexer.py"
198
+ cmd = f"{python_bin} {indexer_script} {repo_path} --output {output_path}"
199
+
200
+ if fetch_pr_info:
201
+ cmd += " --pr-info"
202
+
203
+ # Add spacy model option
204
+ cmd += f" --spacy-model {spacy_model}"
205
+
206
+ _ = run_command(cmd)
207
+
208
+ print(f"✓ Repository indexed at {output_path}")
209
+ return output_path
210
+
211
+
212
+ def detect_installation_method():
213
+ """
214
+ Detect how cicada is installed and return appropriate MCP command config.
215
+
216
+ Returns:
217
+ tuple: (command, args, cwd, description)
218
+ """
219
+ import shutil
220
+ import sys
221
+
222
+ script_path = Path(sys.argv[0]).resolve()
223
+ script_path_str = str(script_path)
224
+
225
+ # Check if running from a uvx cache/temporary directory
226
+ # uvx uses temporary environments, so we should NOT use cicada-server
227
+ # even if it's temporarily in PATH
228
+ uvx_indicators = [
229
+ "/.cache/uv/",
230
+ "/tmp/",
231
+ "tmpdir",
232
+ "temp",
233
+ # On some systems uvx might use other temp locations
234
+ ]
235
+
236
+ is_uvx = any(indicator in script_path_str for indicator in uvx_indicators)
237
+
238
+ if is_uvx:
239
+ # Running from uvx - use Python fallback since cicada-server won't be available later
240
+ python_bin = sys.executable
241
+ cicada_dir = Path(__file__).parent.parent.resolve()
242
+ return (
243
+ str(python_bin),
244
+ [str(cicada_dir / "cicada" / "mcp_server.py")],
245
+ str(cicada_dir),
246
+ "uvx (one-time run, using Python paths)",
247
+ )
248
+
249
+ # Check if running from a uv tools directory (permanent install)
250
+ if (
251
+ ".local/share/uv/tools" in script_path_str
252
+ or ".local/bin/cicada-" in script_path_str
253
+ ):
254
+ # Installed via uv tool install
255
+ return (
256
+ "cicada-server",
257
+ [],
258
+ None,
259
+ "uv tool install (ensure ~/.local/bin is in PATH)",
260
+ )
261
+
262
+ # Check if cicada-server is in PATH (from uv tool install)
263
+ if shutil.which("cicada-server"):
264
+ return ("cicada-server", [], None, "uv tool install (permanent, fast)")
265
+
266
+ # Fall back to python with full path
267
+ python_bin = sys.executable
268
+ cicada_dir = Path(__file__).parent.parent.resolve()
269
+
270
+ return (
271
+ str(python_bin),
272
+ [str(cicada_dir / "cicada" / "mcp_server.py")],
273
+ str(cicada_dir),
274
+ "direct python (tip: install with 'uv tool install .' for faster startup)",
275
+ )
276
+
277
+
278
+ def check_tools_in_path():
279
+ """Check if cicada tools are in PATH."""
280
+ import shutil
281
+
282
+ tools = ["cicada-server", "cicada-index"]
283
+ visible_tools = [tool for tool in tools if shutil.which(tool)]
284
+
285
+ if len(visible_tools) == len(tools):
286
+ return "all_visible"
287
+ elif visible_tools:
288
+ return "partial"
289
+ else:
290
+ return "none"
291
+
292
+
293
+ def create_mcp_config(repo_path, _cicada_dir, _python_bin):
294
+ """Create or update .mcp.json configuration file with intelligent command detection."""
295
+ print("Creating .mcp.json configuration...")
296
+
297
+ repo_path = Path(repo_path).resolve()
298
+ mcp_config_path = repo_path / ".mcp.json"
299
+
300
+ # Load existing config if present, otherwise create new one
301
+ if mcp_config_path.exists():
302
+ try:
303
+ with open(mcp_config_path, "r") as f:
304
+ config = json.load(f)
305
+ print(f"✓ Found existing .mcp.json, will merge configuration")
306
+ except (json.JSONDecodeError, IOError) as e:
307
+ print(f"Warning: Could not read existing .mcp.json ({e}), creating new one")
308
+ config = {}
309
+ else:
310
+ config = {}
311
+
312
+ # Ensure mcpServers section exists
313
+ if "mcpServers" not in config:
314
+ config["mcpServers"] = {}
315
+
316
+ # Detect installation method and create appropriate config
317
+ command, args, cwd, description = detect_installation_method()
318
+
319
+ # Check if tools are visible in PATH
320
+ tools_status = check_tools_in_path()
321
+ if tools_status == "all_visible":
322
+ print(f"✓ Installation: {description}")
323
+ elif tools_status == "partial":
324
+ print(f"⚠️ Installation: {description}")
325
+ print(f" Some tools not found in PATH - add ~/.local/bin to PATH")
326
+ else:
327
+ print(f"⚠️ Installation: {description}")
328
+ print(f" Tools not found in PATH - add ~/.local/bin to PATH")
329
+
330
+ # Build MCP server configuration
331
+ from typing import Any
332
+
333
+ server_config: dict[str, Any] = {"command": command}
334
+
335
+ if args:
336
+ server_config["args"] = args
337
+
338
+ if cwd:
339
+ server_config["cwd"] = cwd
340
+
341
+ # Add environment variable for repo path
342
+ server_config["env"] = {"CICADA_REPO_PATH": str(repo_path)}
343
+
344
+ # Add or update cicada configuration
345
+ config["mcpServers"]["cicada"] = server_config
346
+
347
+ # Write config file
348
+ with open(mcp_config_path, "w") as f:
349
+ json.dump(config, f, indent=2)
350
+
351
+ print(f"✓ MCP configuration updated at {mcp_config_path}")
352
+
353
+ # Show what was configured
354
+ if command == "cicada-server":
355
+ print("✅ Using 'cicada-server' command (fast, no paths needed)")
356
+ else:
357
+ print(f"ℹ️ Using Python: {command}")
358
+
359
+ return mcp_config_path
360
+
361
+
362
+ def create_config_yaml(_cicada_dir, repo_path, index_path):
363
+ """Create or update config.yaml in repository's .cicada directory."""
364
+ repo_path = Path(repo_path).resolve()
365
+ config_path = repo_path / ".cicada" / "config.yaml"
366
+
367
+ # Ensure .cicada directory exists
368
+ config_path.parent.mkdir(parents=True, exist_ok=True)
369
+
370
+ config_content = f"""repository:
371
+ path: {repo_path}
372
+
373
+ storage:
374
+ index_path: {index_path}
375
+ """
376
+
377
+ with open(config_path, "w") as f:
378
+ _ = f.write(config_content)
379
+
380
+ print(f"✓ Config file created at {config_path}")
381
+
382
+
383
+ def create_gitattributes(repo_path):
384
+ """Create or update .gitattributes in repository root for Elixir function tracking."""
385
+ repo_path = Path(repo_path).resolve()
386
+ gitattributes_path = repo_path / ".gitattributes"
387
+
388
+ elixir_patterns = ["*.ex diff=elixir", "*.exs diff=elixir"]
389
+
390
+ # Read existing .gitattributes if present
391
+ existing_lines = []
392
+ if gitattributes_path.exists():
393
+ with open(gitattributes_path, "r") as f:
394
+ existing_lines = [line.rstrip() for line in f.readlines()]
395
+
396
+ # Check if elixir patterns already exist
397
+ has_elixir = any(pattern in existing_lines for pattern in elixir_patterns)
398
+
399
+ if has_elixir:
400
+ print(f"✓ .gitattributes already has Elixir patterns")
401
+ return gitattributes_path
402
+
403
+ # Add elixir patterns
404
+ with open(gitattributes_path, "a") as f:
405
+ if existing_lines and not existing_lines[-1] == "":
406
+ _ = f.write("\n") # Add newline if file doesn't end with one
407
+
408
+ _ = f.write("# Elixir function tracking for git log -L\n")
409
+ for pattern in elixir_patterns:
410
+ _ = f.write(f"{pattern}\n")
411
+
412
+ print(f"✓ Added Elixir patterns to {gitattributes_path}")
413
+ return gitattributes_path
414
+
415
+
416
+ def update_claude_md(repo_path):
417
+ """Update CLAUDE.md with instructions to use cicada-mcp for Elixir codebase searches."""
418
+ import re
419
+ from cicada.mcp_tools import get_tool_definitions
420
+
421
+ repo_path = Path(repo_path).resolve()
422
+ claude_md_path = repo_path / "CLAUDE.md"
423
+
424
+ # Fail silently if CLAUDE.md doesn't exist
425
+ if not claude_md_path.exists():
426
+ return
427
+
428
+ # Auto-generate tool list from mcp_tools.py
429
+ tools = get_tool_definitions()
430
+ tool_list = []
431
+ grep_antipatterns = []
432
+
433
+ for tool in tools:
434
+ # Extract first sentence from description (up to first period or newline)
435
+ desc = tool.description.split("\n")[0].strip()
436
+ if "." in desc:
437
+ desc = desc.split(".")[0] + "."
438
+ tool_list.append(f" - {desc} `mcp__cicada__{tool.name}`")
439
+
440
+ # Get anti-pattern from tool metadata
441
+ if tool.meta and "anti_pattern" in tool.meta:
442
+ grep_antipatterns.append(f" - ❌ {tool.meta['anti_pattern']}")
443
+
444
+ tool_list_str = "\n".join(tool_list)
445
+ grep_antipatterns_str = (
446
+ "\n".join(grep_antipatterns)
447
+ if grep_antipatterns
448
+ else " - ❌ Searching for Elixir code structure"
449
+ )
450
+
451
+ instruction_content = f"""<cicada>
452
+ **ALWAYS use cicada-mcp tools for Elixir code searches. NEVER use Grep/Find for these tasks.**
453
+
454
+ ### Use cicada tools for:
455
+ {tool_list_str}
456
+
457
+ ### DO NOT use Grep for:
458
+ {grep_antipatterns_str}
459
+
460
+ ### You can still use Grep for:
461
+ - ✓ Non-code files (markdown, JSON, config)
462
+ - ✓ String literal searches
463
+ - ✓ Pattern matching in single line comments
464
+ </cicada>
465
+ """
466
+
467
+ try:
468
+ # Read existing content
469
+ with open(claude_md_path, "r") as f:
470
+ content = f.read()
471
+
472
+ # Pattern to find existing <cicada>...</cicada> tags
473
+ cicada_pattern = re.compile(r"<cicada>.*?</cicada>", re.DOTALL)
474
+
475
+ # Check if <cicada> tags exist
476
+ if cicada_pattern.search(content):
477
+ # Replace existing content between tags
478
+ new_content = cicada_pattern.sub(instruction_content, content)
479
+ with open(claude_md_path, "w") as f:
480
+ _ = f.write(new_content)
481
+ print(f"✓ Replaced existing <cicada> instructions in CLAUDE.md")
482
+ elif "cicada-mcp" in content.lower() or "cicada" in content.lower():
483
+ # Content already mentions cicada, don't add duplication
484
+ # This handles cases where users manually added cicada instructions
485
+ print(f"✓ CLAUDE.md already mentions cicada, skipping update")
486
+ else:
487
+ # Append the instruction
488
+ with open(claude_md_path, "a") as f:
489
+ # Add newline if file doesn't end with one
490
+ if content and not content.endswith("\n"):
491
+ _ = f.write("\n")
492
+
493
+ _ = f.write("\n")
494
+ _ = f.write(instruction_content)
495
+
496
+ print(f"✓ Added cicada-mcp usage instructions to CLAUDE.md")
497
+ except Exception:
498
+ # Fail silently on any errors
499
+ pass
500
+
501
+
502
+ def is_gitignored(repo_path, file_pattern):
503
+ """
504
+ Check if a file pattern is in .gitignore.
505
+
506
+ Args:
507
+ repo_path: Path to repository root
508
+ file_pattern: Pattern to check (e.g., '.cicada/', '.mcp.json')
509
+
510
+ Returns:
511
+ bool: True if pattern is in .gitignore, False otherwise
512
+ """
513
+ repo_path = Path(repo_path).resolve()
514
+ gitignore_path = repo_path / ".gitignore"
515
+
516
+ if not gitignore_path.exists():
517
+ return False
518
+
519
+ try:
520
+ with open(gitignore_path, "r") as f:
521
+ content = f.read()
522
+ # Simple check - look for the pattern in the file
523
+ # This handles .cicada/, .cicada, /.cicada/, etc.
524
+ base_pattern = file_pattern.rstrip("/").lstrip("/")
525
+ return base_pattern in content
526
+ except (IOError, OSError):
527
+ return False
528
+
529
+
530
+ def print_setup_summary(repo_path, _index_path):
531
+ """
532
+ Print a summary of created files and their gitignore status.
533
+
534
+ Args:
535
+ repo_path: Path to repository root
536
+ index_path: Path to the created index file
537
+ """
538
+ # ANSI color codes
539
+ YELLOW = "\033[93m"
540
+ RED = "\033[91m"
541
+ GREEN = "\033[92m"
542
+ RESET = "\033[0m"
543
+
544
+ repo_path = Path(repo_path).resolve()
545
+
546
+ print()
547
+ print(f"{YELLOW}Files created/modified:{RESET}")
548
+ print()
549
+
550
+ # List of files to check
551
+ files_created = [
552
+ (".cicada/", "Cicada index directory"),
553
+ (".mcp.json", "MCP server configuration"),
554
+ ]
555
+
556
+ # Check each file
557
+ for file_pattern, description in files_created:
558
+ is_ignored = is_gitignored(repo_path, file_pattern)
559
+ file_path = repo_path / file_pattern.rstrip("/")
560
+
561
+ if file_path.exists():
562
+ status = (
563
+ f"{GREEN}✓ gitignored{RESET}"
564
+ if is_ignored
565
+ else f"{RED}✗ not gitignored{RESET}"
566
+ )
567
+ print(f" {YELLOW}{file_pattern:20}{RESET} {description:35} {status}")
568
+
569
+ print()
570
+
571
+ # Check what needs to be gitignored
572
+ needs_gitignore = []
573
+ if not is_gitignored(repo_path, ".cicada/"):
574
+ needs_gitignore.append(".cicada/")
575
+ if not is_gitignored(repo_path, ".mcp.json"):
576
+ needs_gitignore.append(".mcp.json")
577
+
578
+ # Show warnings if files are not gitignored
579
+ if needs_gitignore:
580
+ print(f"{RED}⚠️ Warning: The following should be in .gitignore:{RESET}")
581
+ for item in needs_gitignore:
582
+ reason = (
583
+ "build artifacts and cache"
584
+ if item == ".cicada/"
585
+ else "local configuration"
586
+ )
587
+ print(f"{RED} • {item:12} ({reason}){RESET}")
588
+ print()
589
+ print(f"{YELLOW}Add them to .gitignore with this command:{RESET}")
590
+ items_with_newlines = "\\n".join(needs_gitignore)
591
+ print(f" printf '\\n{items_with_newlines}\\n' >> .gitignore")
592
+ print()
593
+
594
+
595
+ def main():
596
+ """Main entry point for the setup script."""
597
+ parser = argparse.ArgumentParser(
598
+ description="One-command setup for Cicada MCP server",
599
+ epilog="Example: python setup.py /path/to/elixir/project",
600
+ )
601
+ _ = parser.add_argument(
602
+ "repo",
603
+ nargs="?",
604
+ default=".",
605
+ help="Path to the Elixir repository to index (default: current directory)",
606
+ )
607
+ _ = parser.add_argument(
608
+ "--cicada-dir",
609
+ help="Directory where cicada is or will be installed (default: ~/.cicada)",
610
+ )
611
+ _ = parser.add_argument(
612
+ "--github-url",
613
+ help="GitHub URL to clone cicada from (if not already installed)",
614
+ )
615
+ _ = parser.add_argument(
616
+ "--pr-info",
617
+ action="store_true",
618
+ help="Fetch PR information during indexing (requires GitHub CLI and may be slow)",
619
+ )
620
+ _ = parser.add_argument(
621
+ "--skip-install",
622
+ action="store_true",
623
+ help="Skip installing dependencies (use if already installed)",
624
+ )
625
+ _ = parser.add_argument(
626
+ "--use-uv",
627
+ action="store_true",
628
+ help="Force use of uv for dependency installation (faster)",
629
+ )
630
+ _ = parser.add_argument(
631
+ "--use-pip",
632
+ action="store_true",
633
+ help="Force use of pip for dependency installation (traditional)",
634
+ )
635
+ _ = parser.add_argument(
636
+ "--spacy-model",
637
+ choices=["small", "medium", "large"],
638
+ default="small",
639
+ help="Size of spaCy model to use for keyword extraction (default: small). "
640
+ "Medium and large models provide better accuracy but are slower.",
641
+ )
642
+
643
+ args = parser.parse_args()
644
+
645
+ print("=" * 60)
646
+ print("Cicada MCP Setup")
647
+ print("=" * 60)
648
+
649
+ # Check Python version
650
+ check_python()
651
+
652
+ # Determine cicada directory
653
+ if args.cicada_dir:
654
+ cicada_dir = Path(args.cicada_dir).resolve()
655
+ else:
656
+ # Use current directory if we're in cicada, otherwise use ~/.cicada
657
+ current_dir = Path.cwd()
658
+ if (current_dir / "cicada" / "mcp_server.py").exists():
659
+ cicada_dir = current_dir
660
+ else:
661
+ cicada_dir = Path.home() / ".cicada"
662
+
663
+ # Install or locate cicada
664
+ cicada_dir, is_already_installed = install_cicada(cicada_dir, args.github_url)
665
+
666
+ # Install dependencies (skip if already installed via pip/uvx)
667
+ if is_already_installed:
668
+ # Package already installed, use current Python
669
+ python_bin = sys.executable
670
+ print(f"✓ Using Python from installed package: {python_bin}")
671
+ elif not args.skip_install:
672
+ # Determine which package manager to use
673
+ use_uv = None
674
+ if args.use_uv:
675
+ use_uv = True
676
+ elif args.use_pip:
677
+ use_uv = False
678
+ # Otherwise use_uv=None for auto-detect
679
+
680
+ python_bin = install_dependencies(cicada_dir, use_uv=use_uv)
681
+ else:
682
+ # Try to find existing python binary
683
+ python_bin = cicada_dir / ".venv" / "bin" / "python"
684
+ if not python_bin.exists():
685
+ python_bin = cicada_dir / "venv" / "bin" / "python"
686
+ if not python_bin.exists():
687
+ python_bin = sys.executable
688
+ print(f"✓ Skipping dependency installation, using {python_bin}")
689
+
690
+ # Index repository
691
+ index_path = index_repository(
692
+ cicada_dir, python_bin, args.repo, args.pr_info, args.spacy_model
693
+ )
694
+
695
+ # Create config.yaml
696
+ create_config_yaml(cicada_dir, args.repo, index_path)
697
+
698
+ # Create .gitattributes for Elixir function tracking
699
+ _ = create_gitattributes(args.repo)
700
+
701
+ # Update CLAUDE.md with cicada-mcp usage instructions
702
+ update_claude_md(args.repo)
703
+
704
+ # Create .mcp.json
705
+ _ = create_mcp_config(args.repo, cicada_dir, python_bin)
706
+
707
+ # Print summary of created files and gitignore status
708
+ print_setup_summary(args.repo, index_path)
709
+
710
+ print("=" * 60)
711
+ print("✓ Setup Complete!")
712
+ print("=" * 60)
713
+ print()
714
+ print("Next steps:")
715
+ print("1. Restart Claude Code")
716
+ print()
717
+ print("2. Try asking Claude Code:")
718
+ print(" - 'Where is [Module] used?'")
719
+ print(" - 'Show me the functions in [ModuleName]'")
720
+ print()
721
+
722
+
723
+ if __name__ == "__main__":
724
+ main()