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/install.py DELETED
@@ -1,741 +0,0 @@
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 - check for cicada-mcp first
255
- if shutil.which("cicada-mcp"):
256
- return (
257
- "cicada-mcp",
258
- [],
259
- None,
260
- "uv tool install (ensure ~/.local/bin is in PATH)",
261
- )
262
- # Fall back to cicada-server for backwards compatibility
263
- return (
264
- "cicada-server",
265
- [],
266
- None,
267
- "uv tool install (ensure ~/.local/bin is in PATH)",
268
- )
269
-
270
- # Check if cicada-mcp is in PATH first (from uv tool install)
271
- if shutil.which("cicada-mcp"):
272
- return ("cicada-mcp", [], None, "uv tool install (permanent, fast)")
273
-
274
- # Fall back to cicada-server for backwards compatibility
275
- if shutil.which("cicada-server"):
276
- return ("cicada-server", [], None, "uv tool install (permanent, fast)")
277
-
278
- # Fall back to python with full path
279
- python_bin = sys.executable
280
- cicada_dir = Path(__file__).parent.parent.resolve()
281
-
282
- return (
283
- str(python_bin),
284
- [str(cicada_dir / "cicada" / "mcp_server.py")],
285
- str(cicada_dir),
286
- "direct python (tip: install with 'uv tool install .' for faster startup)",
287
- )
288
-
289
-
290
- def check_tools_in_path():
291
- """Check if cicada tools are in PATH."""
292
- import shutil
293
-
294
- # Check for cicada-mcp (new) or cicada-server (backwards compat)
295
- has_mcp_server = shutil.which("cicada-mcp") or shutil.which("cicada-server")
296
- tools = ["cicada-index"]
297
- visible_tools = [tool for tool in tools if shutil.which(tool)]
298
- if has_mcp_server:
299
- visible_tools.insert(0, "cicada-mcp/cicada-server")
300
- tools.insert(0, "cicada-mcp/cicada-server")
301
-
302
- if len(visible_tools) == len(tools):
303
- return "all_visible"
304
- elif visible_tools:
305
- return "partial"
306
- else:
307
- return "none"
308
-
309
-
310
- def create_mcp_config(repo_path, _cicada_dir, _python_bin):
311
- """Create or update .mcp.json configuration file with intelligent command detection."""
312
- print("Creating .mcp.json configuration...")
313
-
314
- repo_path = Path(repo_path).resolve()
315
- mcp_config_path = repo_path / ".mcp.json"
316
-
317
- # Load existing config if present, otherwise create new one
318
- if mcp_config_path.exists():
319
- try:
320
- with open(mcp_config_path, "r") as f:
321
- config = json.load(f)
322
- print(f"✓ Found existing .mcp.json, will merge configuration")
323
- except (json.JSONDecodeError, IOError) as e:
324
- print(f"Warning: Could not read existing .mcp.json ({e}), creating new one")
325
- config = {}
326
- else:
327
- config = {}
328
-
329
- # Ensure mcpServers section exists
330
- if "mcpServers" not in config:
331
- config["mcpServers"] = {}
332
-
333
- # Detect installation method and create appropriate config
334
- command, args, cwd, description = detect_installation_method()
335
-
336
- # Check if tools are visible in PATH
337
- tools_status = check_tools_in_path()
338
- if tools_status == "all_visible":
339
- print(f"✓ Installation: {description}")
340
- elif tools_status == "partial":
341
- print(f"⚠️ Installation: {description}")
342
- print(f" Some tools not found in PATH - add ~/.local/bin to PATH")
343
- else:
344
- print(f"⚠️ Installation: {description}")
345
- print(f" Tools not found in PATH - add ~/.local/bin to PATH")
346
-
347
- # Build MCP server configuration
348
- from typing import Any
349
-
350
- server_config: dict[str, Any] = {"command": command}
351
-
352
- if args:
353
- server_config["args"] = args
354
-
355
- if cwd:
356
- server_config["cwd"] = cwd
357
-
358
- # Add environment variable for repo path
359
- server_config["env"] = {"CICADA_REPO_PATH": str(repo_path)}
360
-
361
- # Add or update cicada configuration
362
- config["mcpServers"]["cicada"] = server_config
363
-
364
- # Write config file
365
- with open(mcp_config_path, "w") as f:
366
- json.dump(config, f, indent=2)
367
-
368
- print(f"✓ MCP configuration updated at {mcp_config_path}")
369
-
370
- # Show what was configured
371
- if command in ("cicada-mcp", "cicada-server"):
372
- print(f"✅ Using '{command}' command (fast, no paths needed)")
373
- else:
374
- print(f"ℹ️ Using Python: {command}")
375
-
376
- return mcp_config_path
377
-
378
-
379
- def create_config_yaml(_cicada_dir, repo_path, index_path):
380
- """Create or update config.yaml in repository's .cicada directory."""
381
- repo_path = Path(repo_path).resolve()
382
- config_path = repo_path / ".cicada" / "config.yaml"
383
-
384
- # Ensure .cicada directory exists
385
- config_path.parent.mkdir(parents=True, exist_ok=True)
386
-
387
- config_content = f"""repository:
388
- path: {repo_path}
389
-
390
- storage:
391
- index_path: {index_path}
392
- """
393
-
394
- with open(config_path, "w") as f:
395
- _ = f.write(config_content)
396
-
397
- print(f"✓ Config file created at {config_path}")
398
-
399
-
400
- def create_gitattributes(repo_path):
401
- """Create or update .gitattributes in repository root for Elixir function tracking."""
402
- repo_path = Path(repo_path).resolve()
403
- gitattributes_path = repo_path / ".gitattributes"
404
-
405
- elixir_patterns = ["*.ex diff=elixir", "*.exs diff=elixir"]
406
-
407
- # Read existing .gitattributes if present
408
- existing_lines = []
409
- if gitattributes_path.exists():
410
- with open(gitattributes_path, "r") as f:
411
- existing_lines = [line.rstrip() for line in f.readlines()]
412
-
413
- # Check if elixir patterns already exist
414
- has_elixir = any(pattern in existing_lines for pattern in elixir_patterns)
415
-
416
- if has_elixir:
417
- print(f"✓ .gitattributes already has Elixir patterns")
418
- return gitattributes_path
419
-
420
- # Add elixir patterns
421
- with open(gitattributes_path, "a") as f:
422
- if existing_lines and not existing_lines[-1] == "":
423
- _ = f.write("\n") # Add newline if file doesn't end with one
424
-
425
- _ = f.write("# Elixir function tracking for git log -L\n")
426
- for pattern in elixir_patterns:
427
- _ = f.write(f"{pattern}\n")
428
-
429
- print(f"✓ Added Elixir patterns to {gitattributes_path}")
430
- return gitattributes_path
431
-
432
-
433
- def update_claude_md(repo_path):
434
- """Update CLAUDE.md with instructions to use cicada-mcp for Elixir codebase searches."""
435
- import re
436
- from cicada.mcp_tools import get_tool_definitions
437
-
438
- repo_path = Path(repo_path).resolve()
439
- claude_md_path = repo_path / "CLAUDE.md"
440
-
441
- # Fail silently if CLAUDE.md doesn't exist
442
- if not claude_md_path.exists():
443
- return
444
-
445
- # Auto-generate tool list from mcp_tools.py
446
- tools = get_tool_definitions()
447
- tool_list = []
448
- grep_antipatterns = []
449
-
450
- for tool in tools:
451
- # Extract first sentence from description (up to first period or newline)
452
- desc = tool.description.split("\n")[0].strip()
453
- if "." in desc:
454
- desc = desc.split(".")[0] + "."
455
- tool_list.append(f" - {desc} `mcp__cicada__{tool.name}`")
456
-
457
- # Get anti-pattern from tool metadata
458
- if tool.meta and "anti_pattern" in tool.meta:
459
- grep_antipatterns.append(f" - ❌ {tool.meta['anti_pattern']}")
460
-
461
- tool_list_str = "\n".join(tool_list)
462
- grep_antipatterns_str = (
463
- "\n".join(grep_antipatterns)
464
- if grep_antipatterns
465
- else " - ❌ Searching for Elixir code structure"
466
- )
467
-
468
- instruction_content = f"""<cicada>
469
- **ALWAYS use cicada-mcp tools for Elixir code searches. NEVER use Grep/Find for these tasks.**
470
-
471
- ### Use cicada tools for:
472
- {tool_list_str}
473
-
474
- ### DO NOT use Grep for:
475
- {grep_antipatterns_str}
476
-
477
- ### You can still use Grep for:
478
- - ✓ Non-code files (markdown, JSON, config)
479
- - ✓ String literal searches
480
- - ✓ Pattern matching in single line comments
481
- </cicada>
482
- """
483
-
484
- try:
485
- # Read existing content
486
- with open(claude_md_path, "r") as f:
487
- content = f.read()
488
-
489
- # Pattern to find existing <cicada>...</cicada> tags
490
- cicada_pattern = re.compile(r"<cicada>.*?</cicada>", re.DOTALL)
491
-
492
- # Check if <cicada> tags exist
493
- if cicada_pattern.search(content):
494
- # Replace existing content between tags
495
- new_content = cicada_pattern.sub(instruction_content, content)
496
- with open(claude_md_path, "w") as f:
497
- _ = f.write(new_content)
498
- print(f"✓ Replaced existing <cicada> instructions in CLAUDE.md")
499
- elif "cicada-mcp" in content.lower() or "cicada" in content.lower():
500
- # Content already mentions cicada, don't add duplication
501
- # This handles cases where users manually added cicada instructions
502
- print(f"✓ CLAUDE.md already mentions cicada, skipping update")
503
- else:
504
- # Append the instruction
505
- with open(claude_md_path, "a") as f:
506
- # Add newline if file doesn't end with one
507
- if content and not content.endswith("\n"):
508
- _ = f.write("\n")
509
-
510
- _ = f.write("\n")
511
- _ = f.write(instruction_content)
512
-
513
- print(f"✓ Added cicada-mcp usage instructions to CLAUDE.md")
514
- except Exception:
515
- # Fail silently on any errors
516
- pass
517
-
518
-
519
- def is_gitignored(repo_path, file_pattern):
520
- """
521
- Check if a file pattern is in .gitignore.
522
-
523
- Args:
524
- repo_path: Path to repository root
525
- file_pattern: Pattern to check (e.g., '.cicada/', '.mcp.json')
526
-
527
- Returns:
528
- bool: True if pattern is in .gitignore, False otherwise
529
- """
530
- repo_path = Path(repo_path).resolve()
531
- gitignore_path = repo_path / ".gitignore"
532
-
533
- if not gitignore_path.exists():
534
- return False
535
-
536
- try:
537
- with open(gitignore_path, "r") as f:
538
- content = f.read()
539
- # Simple check - look for the pattern in the file
540
- # This handles .cicada/, .cicada, /.cicada/, etc.
541
- base_pattern = file_pattern.rstrip("/").lstrip("/")
542
- return base_pattern in content
543
- except (IOError, OSError):
544
- return False
545
-
546
-
547
- def print_setup_summary(repo_path, _index_path):
548
- """
549
- Print a summary of created files and their gitignore status.
550
-
551
- Args:
552
- repo_path: Path to repository root
553
- index_path: Path to the created index file
554
- """
555
- # ANSI color codes
556
- YELLOW = "\033[93m"
557
- RED = "\033[91m"
558
- GREEN = "\033[92m"
559
- RESET = "\033[0m"
560
-
561
- repo_path = Path(repo_path).resolve()
562
-
563
- print()
564
- print(f"{YELLOW}Files created/modified:{RESET}")
565
- print()
566
-
567
- # List of files to check
568
- files_created = [
569
- (".cicada/", "Cicada index directory"),
570
- (".mcp.json", "MCP server configuration"),
571
- ]
572
-
573
- # Check each file
574
- for file_pattern, description in files_created:
575
- is_ignored = is_gitignored(repo_path, file_pattern)
576
- file_path = repo_path / file_pattern.rstrip("/")
577
-
578
- if file_path.exists():
579
- status = (
580
- f"{GREEN}✓ gitignored{RESET}"
581
- if is_ignored
582
- else f"{RED}✗ not gitignored{RESET}"
583
- )
584
- print(f" {YELLOW}{file_pattern:20}{RESET} {description:35} {status}")
585
-
586
- print()
587
-
588
- # Check what needs to be gitignored
589
- needs_gitignore = []
590
- if not is_gitignored(repo_path, ".cicada/"):
591
- needs_gitignore.append(".cicada/")
592
- if not is_gitignored(repo_path, ".mcp.json"):
593
- needs_gitignore.append(".mcp.json")
594
-
595
- # Show warnings if files are not gitignored
596
- if needs_gitignore:
597
- print(f"{RED}⚠️ Warning: The following should be in .gitignore:{RESET}")
598
- for item in needs_gitignore:
599
- reason = (
600
- "build artifacts and cache"
601
- if item == ".cicada/"
602
- else "local configuration"
603
- )
604
- print(f"{RED} • {item:12} ({reason}){RESET}")
605
- print()
606
- print(f"{YELLOW}Add them to .gitignore with this command:{RESET}")
607
- items_with_newlines = "\\n".join(needs_gitignore)
608
- print(f" printf '\\n{items_with_newlines}\\n' >> .gitignore")
609
- print()
610
-
611
-
612
- def main():
613
- """Main entry point for the setup script."""
614
- parser = argparse.ArgumentParser(
615
- description="One-command setup for Cicada MCP server",
616
- epilog="Example: python setup.py /path/to/elixir/project",
617
- )
618
- _ = parser.add_argument(
619
- "repo",
620
- nargs="?",
621
- default=".",
622
- help="Path to the Elixir repository to index (default: current directory)",
623
- )
624
- _ = parser.add_argument(
625
- "--cicada-dir",
626
- help="Directory where cicada is or will be installed (default: ~/.cicada)",
627
- )
628
- _ = parser.add_argument(
629
- "--github-url",
630
- help="GitHub URL to clone cicada from (if not already installed)",
631
- )
632
- _ = parser.add_argument(
633
- "--pr-info",
634
- action="store_true",
635
- help="Fetch PR information during indexing (requires GitHub CLI and may be slow)",
636
- )
637
- _ = parser.add_argument(
638
- "--skip-install",
639
- action="store_true",
640
- help="Skip installing dependencies (use if already installed)",
641
- )
642
- _ = parser.add_argument(
643
- "--use-uv",
644
- action="store_true",
645
- help="Force use of uv for dependency installation (faster)",
646
- )
647
- _ = parser.add_argument(
648
- "--use-pip",
649
- action="store_true",
650
- help="Force use of pip for dependency installation (traditional)",
651
- )
652
- _ = parser.add_argument(
653
- "--spacy-model",
654
- choices=["small", "medium", "large"],
655
- default="small",
656
- help="Size of spaCy model to use for keyword extraction (default: small). "
657
- "Medium and large models provide better accuracy but are slower.",
658
- )
659
-
660
- args = parser.parse_args()
661
-
662
- print("=" * 60)
663
- print("Cicada MCP Setup")
664
- print("=" * 60)
665
-
666
- # Check Python version
667
- check_python()
668
-
669
- # Determine cicada directory
670
- if args.cicada_dir:
671
- cicada_dir = Path(args.cicada_dir).resolve()
672
- else:
673
- # Use current directory if we're in cicada, otherwise use ~/.cicada
674
- current_dir = Path.cwd()
675
- if (current_dir / "cicada" / "mcp_server.py").exists():
676
- cicada_dir = current_dir
677
- else:
678
- cicada_dir = Path.home() / ".cicada"
679
-
680
- # Install or locate cicada
681
- cicada_dir, is_already_installed = install_cicada(cicada_dir, args.github_url)
682
-
683
- # Install dependencies (skip if already installed via pip/uvx)
684
- if is_already_installed:
685
- # Package already installed, use current Python
686
- python_bin = sys.executable
687
- print(f"✓ Using Python from installed package: {python_bin}")
688
- elif not args.skip_install:
689
- # Determine which package manager to use
690
- use_uv = None
691
- if args.use_uv:
692
- use_uv = True
693
- elif args.use_pip:
694
- use_uv = False
695
- # Otherwise use_uv=None for auto-detect
696
-
697
- python_bin = install_dependencies(cicada_dir, use_uv=use_uv)
698
- else:
699
- # Try to find existing python binary
700
- python_bin = cicada_dir / ".venv" / "bin" / "python"
701
- if not python_bin.exists():
702
- python_bin = cicada_dir / "venv" / "bin" / "python"
703
- if not python_bin.exists():
704
- python_bin = sys.executable
705
- print(f"✓ Skipping dependency installation, using {python_bin}")
706
-
707
- # Index repository
708
- index_path = index_repository(
709
- cicada_dir, python_bin, args.repo, args.pr_info, args.spacy_model
710
- )
711
-
712
- # Create config.yaml
713
- create_config_yaml(cicada_dir, args.repo, index_path)
714
-
715
- # Create .gitattributes for Elixir function tracking
716
- _ = create_gitattributes(args.repo)
717
-
718
- # Update CLAUDE.md with cicada-mcp usage instructions
719
- update_claude_md(args.repo)
720
-
721
- # Create .mcp.json
722
- _ = create_mcp_config(args.repo, cicada_dir, python_bin)
723
-
724
- # Print summary of created files and gitignore status
725
- print_setup_summary(args.repo, index_path)
726
-
727
- print("=" * 60)
728
- print("✓ Setup Complete!")
729
- print("=" * 60)
730
- print()
731
- print("Next steps:")
732
- print("1. Restart Claude Code")
733
- print()
734
- print("2. Try asking Claude Code:")
735
- print(" - 'Where is [Module] used?'")
736
- print(" - 'Show me the functions in [ModuleName]'")
737
- print()
738
-
739
-
740
- if __name__ == "__main__":
741
- main()