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/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()
|