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
|
@@ -6,9 +6,10 @@ separating index construction logic from API and mapping concerns.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
|
-
from cicada.utils import load_index as load_index_util
|
|
11
|
+
from cicada.utils import load_index as load_index_util
|
|
12
|
+
from cicada.utils import save_index as save_index_util
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class PRIndexBuilder:
|
|
@@ -31,8 +32,8 @@ class PRIndexBuilder:
|
|
|
31
32
|
self.repo_name = repo_name
|
|
32
33
|
|
|
33
34
|
def build_index(
|
|
34
|
-
self, prs:
|
|
35
|
-
) ->
|
|
35
|
+
self, prs: list[dict[str, Any]], preserve_last_pr: int | None = None
|
|
36
|
+
) -> dict[str, Any]:
|
|
36
37
|
"""
|
|
37
38
|
Build the index structure from PR data.
|
|
38
39
|
|
|
@@ -56,7 +57,7 @@ class PRIndexBuilder:
|
|
|
56
57
|
total_comments = sum(len(pr.get("comments", [])) for pr in prs)
|
|
57
58
|
|
|
58
59
|
# Build index structure
|
|
59
|
-
metadata:
|
|
60
|
+
metadata: dict[str, Any] = {
|
|
60
61
|
"repo_owner": self.repo_owner,
|
|
61
62
|
"repo_name": self.repo_name,
|
|
62
63
|
"last_indexed_at": datetime.now().isoformat(),
|
|
@@ -65,7 +66,7 @@ class PRIndexBuilder:
|
|
|
65
66
|
"total_comments": total_comments,
|
|
66
67
|
"total_files": len(file_to_prs),
|
|
67
68
|
}
|
|
68
|
-
index:
|
|
69
|
+
index: dict[str, Any] = {
|
|
69
70
|
"metadata": metadata,
|
|
70
71
|
"prs": {str(pr["number"]): pr for pr in prs},
|
|
71
72
|
"commit_to_pr": commit_to_pr,
|
|
@@ -86,7 +87,7 @@ class PRIndexBuilder:
|
|
|
86
87
|
)
|
|
87
88
|
return index
|
|
88
89
|
|
|
89
|
-
def _build_commit_mapping(self, prs:
|
|
90
|
+
def _build_commit_mapping(self, prs: list[dict[str, Any]]) -> dict[str, int]:
|
|
90
91
|
"""
|
|
91
92
|
Build commit SHA -> PR number mapping.
|
|
92
93
|
|
|
@@ -103,7 +104,7 @@ class PRIndexBuilder:
|
|
|
103
104
|
commit_to_pr[commit] = pr_number
|
|
104
105
|
return commit_to_pr
|
|
105
106
|
|
|
106
|
-
def _build_file_mapping(self, prs:
|
|
107
|
+
def _build_file_mapping(self, prs: list[dict[str, Any]]) -> dict[str, list[int]]:
|
|
107
108
|
"""
|
|
108
109
|
Build file path -> PR numbers mapping.
|
|
109
110
|
|
|
@@ -128,8 +129,8 @@ class PRIndexBuilder:
|
|
|
128
129
|
return file_to_prs
|
|
129
130
|
|
|
130
131
|
def merge_indexes(
|
|
131
|
-
self, existing_index:
|
|
132
|
-
) ->
|
|
132
|
+
self, existing_index: dict[str, Any], new_prs: list[dict[str, Any]]
|
|
133
|
+
) -> dict[str, Any]:
|
|
133
134
|
"""
|
|
134
135
|
Merge new PRs into existing index.
|
|
135
136
|
|
|
@@ -155,29 +156,25 @@ class PRIndexBuilder:
|
|
|
155
156
|
existing_index["file_to_prs"] = file_to_prs
|
|
156
157
|
|
|
157
158
|
# Count total comments
|
|
158
|
-
total_comments = sum(
|
|
159
|
-
len(pr.get("comments", [])) for pr in existing_index["prs"].values()
|
|
160
|
-
)
|
|
159
|
+
total_comments = sum(len(pr.get("comments", [])) for pr in existing_index["prs"].values())
|
|
161
160
|
|
|
162
161
|
# Update metadata
|
|
163
162
|
existing_index["metadata"]["last_indexed_at"] = datetime.now().isoformat()
|
|
164
163
|
existing_index["metadata"]["total_prs"] = len(existing_index["prs"])
|
|
165
|
-
existing_index["metadata"]["total_commits_mapped"] = len(
|
|
166
|
-
existing_index["commit_to_pr"]
|
|
167
|
-
)
|
|
164
|
+
existing_index["metadata"]["total_commits_mapped"] = len(existing_index["commit_to_pr"])
|
|
168
165
|
existing_index["metadata"]["total_comments"] = total_comments
|
|
169
166
|
existing_index["metadata"]["total_files"] = len(file_to_prs)
|
|
170
167
|
|
|
171
168
|
# Update last_pr_number to the highest PR we have in the index
|
|
172
169
|
if existing_index["prs"]:
|
|
173
|
-
all_pr_numbers = [int(num) for num in existing_index["prs"]
|
|
170
|
+
all_pr_numbers = [int(num) for num in existing_index["prs"]]
|
|
174
171
|
existing_index["metadata"]["last_pr_number"] = max(all_pr_numbers)
|
|
175
172
|
|
|
176
173
|
return existing_index
|
|
177
174
|
|
|
178
175
|
def merge_partial_clean(
|
|
179
|
-
self, existing_index:
|
|
180
|
-
) ->
|
|
176
|
+
self, existing_index: dict[str, Any], partial_index: dict[str, Any]
|
|
177
|
+
) -> dict[str, Any]:
|
|
181
178
|
"""
|
|
182
179
|
Merge a partial clean build with an existing index.
|
|
183
180
|
|
|
@@ -202,17 +199,13 @@ class PRIndexBuilder:
|
|
|
202
199
|
merged["prs"][pr_num_str] = pr_data
|
|
203
200
|
|
|
204
201
|
# Rebuild commit -> PR mapping from scratch
|
|
205
|
-
merged["commit_to_pr"] = self._build_commit_mapping(
|
|
206
|
-
list(merged["prs"].values())
|
|
207
|
-
)
|
|
202
|
+
merged["commit_to_pr"] = self._build_commit_mapping(list(merged["prs"].values()))
|
|
208
203
|
|
|
209
204
|
# Rebuild file -> PRs mapping from scratch
|
|
210
205
|
merged["file_to_prs"] = self._build_file_mapping(list(merged["prs"].values()))
|
|
211
206
|
|
|
212
207
|
# Count total comments
|
|
213
|
-
total_comments = sum(
|
|
214
|
-
len(pr.get("comments", [])) for pr in merged["prs"].values()
|
|
215
|
-
)
|
|
208
|
+
total_comments = sum(len(pr.get("comments", [])) for pr in merged["prs"].values())
|
|
216
209
|
|
|
217
210
|
# Update metadata (use partial_index's last_pr_number which was preserved)
|
|
218
211
|
merged["metadata"]["last_indexed_at"] = datetime.now().isoformat()
|
|
@@ -220,17 +213,12 @@ class PRIndexBuilder:
|
|
|
220
213
|
merged["metadata"]["total_commits_mapped"] = len(merged["commit_to_pr"])
|
|
221
214
|
merged["metadata"]["total_comments"] = total_comments
|
|
222
215
|
merged["metadata"]["total_files"] = len(merged["file_to_prs"])
|
|
223
|
-
merged["metadata"]["last_pr_number"] = partial_index["metadata"].get(
|
|
224
|
-
"last_pr_number", 0
|
|
225
|
-
)
|
|
216
|
+
merged["metadata"]["last_pr_number"] = partial_index["metadata"].get("last_pr_number", 0)
|
|
226
217
|
|
|
227
|
-
print(
|
|
228
|
-
f"Merged: {len(merged['prs'])} total PRs "
|
|
229
|
-
f"({len(partial_index['prs'])} new/updated)"
|
|
230
|
-
)
|
|
218
|
+
print(f"Merged: {len(merged['prs'])} total PRs ({len(partial_index['prs'])} new/updated)")
|
|
231
219
|
return merged
|
|
232
220
|
|
|
233
|
-
def load_existing_index(self, index_path: str) ->
|
|
221
|
+
def load_existing_index(self, index_path: str) -> dict[str, Any] | None:
|
|
234
222
|
"""
|
|
235
223
|
Load existing index file if it exists.
|
|
236
224
|
|
|
@@ -242,7 +230,7 @@ class PRIndexBuilder:
|
|
|
242
230
|
"""
|
|
243
231
|
return load_index_util(index_path, verbose=True, raise_on_error=False)
|
|
244
232
|
|
|
245
|
-
def save_index(self, index:
|
|
233
|
+
def save_index(self, index: dict[str, Any], output_path: str) -> None:
|
|
246
234
|
"""
|
|
247
235
|
Save index to file.
|
|
248
236
|
|
cicada/setup.py
CHANGED
|
@@ -2,28 +2,26 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Cicada Simplified Setup Script.
|
|
4
4
|
|
|
5
|
-
One-command setup: uvx cicada [claude|cursor|vs]
|
|
5
|
+
One-command setup: uvx --from cicada-mcp cicada [claude|cursor|vs]
|
|
6
6
|
- Indexes the repository with keyword extraction
|
|
7
7
|
- Stores all files in temp directory (~/.cicada/projects/<hash>/)
|
|
8
8
|
- Creates only MCP config file in user's repo
|
|
9
|
+
- Generates MCP config that uses 'uvx cicada-mcp' (works with or without permanent install)
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
import argparse
|
|
12
13
|
import json
|
|
13
14
|
import sys
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import Literal
|
|
16
|
+
from typing import Any, Literal, cast
|
|
16
17
|
|
|
17
18
|
from cicada.indexer import ElixirIndexer
|
|
18
19
|
from cicada.utils import (
|
|
19
20
|
create_storage_dir,
|
|
20
|
-
get_index_path,
|
|
21
21
|
get_config_path,
|
|
22
|
-
|
|
23
|
-
get_storage_dir,
|
|
22
|
+
get_index_path,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
|
-
|
|
27
25
|
EditorType = Literal["claude", "cursor", "vs"]
|
|
28
26
|
|
|
29
27
|
|
|
@@ -41,21 +39,19 @@ def _load_existing_config(config_path: Path) -> dict:
|
|
|
41
39
|
return {}
|
|
42
40
|
|
|
43
41
|
try:
|
|
44
|
-
with open(config_path
|
|
42
|
+
with open(config_path) as f:
|
|
45
43
|
return json.load(f)
|
|
46
44
|
except json.JSONDecodeError as e:
|
|
47
|
-
print(
|
|
48
|
-
f"Warning: Existing config at {config_path} is malformed, creating new one: {e}"
|
|
49
|
-
)
|
|
45
|
+
print(f"Warning: Existing config at {config_path} is malformed, creating new one: {e}")
|
|
50
46
|
return {}
|
|
51
|
-
except
|
|
47
|
+
except OSError as e:
|
|
52
48
|
print(f"Warning: Could not read config file {config_path}: {e}")
|
|
53
49
|
return {}
|
|
54
50
|
|
|
55
51
|
|
|
56
52
|
def _build_server_config(
|
|
57
53
|
command: str, args: list, cwd: str | None, repo_path: Path, storage_dir: Path
|
|
58
|
-
) -> dict:
|
|
54
|
+
) -> dict[str, Any]:
|
|
59
55
|
"""
|
|
60
56
|
Build the MCP server configuration.
|
|
61
57
|
|
|
@@ -69,7 +65,7 @@ def _build_server_config(
|
|
|
69
65
|
Returns:
|
|
70
66
|
Server configuration dict
|
|
71
67
|
"""
|
|
72
|
-
server_config = {"command": command}
|
|
68
|
+
server_config: dict[str, Any] = {"command": command}
|
|
73
69
|
|
|
74
70
|
if args:
|
|
75
71
|
server_config["args"] = args
|
|
@@ -99,26 +95,11 @@ def get_mcp_config_for_editor(
|
|
|
99
95
|
Returns:
|
|
100
96
|
Tuple of (config_file_path, config_content)
|
|
101
97
|
"""
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
has_cicada_server = shutil.which("cicada-server") is not None
|
|
108
|
-
|
|
109
|
-
if has_cicada_mcp:
|
|
110
|
-
command = "cicada-mcp"
|
|
111
|
-
args = []
|
|
112
|
-
cwd = None
|
|
113
|
-
elif has_cicada_server:
|
|
114
|
-
command = "cicada-server"
|
|
115
|
-
args = []
|
|
116
|
-
cwd = None
|
|
117
|
-
else:
|
|
118
|
-
python_bin = sys.executable
|
|
119
|
-
command = str(python_bin)
|
|
120
|
-
args = ["-m", "cicada.mcp_server"]
|
|
121
|
-
cwd = None
|
|
98
|
+
# Always use uvx for maximum compatibility
|
|
99
|
+
# Works whether cicada-mcp is permanently installed or not
|
|
100
|
+
command = "uvx"
|
|
101
|
+
args = ["cicada-mcp"]
|
|
102
|
+
cwd = None
|
|
122
103
|
|
|
123
104
|
# Editor-specific specifications
|
|
124
105
|
editor_specs = {
|
|
@@ -143,7 +124,7 @@ def get_mcp_config_for_editor(
|
|
|
143
124
|
raise ValueError(f"Unsupported editor: {editor}")
|
|
144
125
|
|
|
145
126
|
spec = editor_specs[editor]
|
|
146
|
-
config_path = spec["config_path"]
|
|
127
|
+
config_path = cast(Path, spec["config_path"])
|
|
147
128
|
|
|
148
129
|
# Create parent directory if needed
|
|
149
130
|
if spec["needs_dir"]:
|
|
@@ -163,134 +144,262 @@ def get_mcp_config_for_editor(
|
|
|
163
144
|
return config_path, config
|
|
164
145
|
|
|
165
146
|
|
|
166
|
-
def create_config_yaml(
|
|
147
|
+
def create_config_yaml(
|
|
148
|
+
repo_path: Path,
|
|
149
|
+
storage_dir: Path,
|
|
150
|
+
keyword_method: str | None = None,
|
|
151
|
+
keyword_tier: str | None = None,
|
|
152
|
+
verbose: bool = True,
|
|
153
|
+
) -> None:
|
|
167
154
|
"""
|
|
168
155
|
Create config.yaml in storage directory.
|
|
169
156
|
|
|
170
157
|
Args:
|
|
171
158
|
repo_path: Path to the repository
|
|
172
159
|
storage_dir: Path to the storage directory
|
|
160
|
+
keyword_method: Keyword extraction method ('lemminflect' or 'bert'), None for default
|
|
161
|
+
keyword_tier: Model tier ('fast', 'regular', 'max'), None for default
|
|
162
|
+
verbose: If True, print success message. If False, silently create config.
|
|
173
163
|
"""
|
|
174
164
|
config_path = get_config_path(repo_path)
|
|
175
165
|
index_path = get_index_path(repo_path)
|
|
176
166
|
|
|
167
|
+
# Default to lemminflect if not specified
|
|
168
|
+
if keyword_method is None:
|
|
169
|
+
keyword_method = "lemminflect"
|
|
170
|
+
if keyword_tier is None:
|
|
171
|
+
keyword_tier = "regular"
|
|
172
|
+
|
|
177
173
|
config_content = f"""repository:
|
|
178
174
|
path: {repo_path}
|
|
179
175
|
|
|
180
176
|
storage:
|
|
181
177
|
index_path: {index_path}
|
|
178
|
+
|
|
179
|
+
keyword_extraction:
|
|
180
|
+
method: {keyword_method}
|
|
181
|
+
tier: {keyword_tier}
|
|
182
182
|
"""
|
|
183
183
|
|
|
184
184
|
with open(config_path, "w") as f:
|
|
185
185
|
f.write(config_content)
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
if verbose:
|
|
188
|
+
print(f"✓ Config file created at {config_path}")
|
|
188
189
|
|
|
189
190
|
|
|
190
|
-
def index_repository(repo_path: Path) -> None:
|
|
191
|
+
def index_repository(repo_path: Path, force_full: bool = False, verbose: bool = True) -> None:
|
|
191
192
|
"""
|
|
192
193
|
Index the repository with keyword extraction enabled.
|
|
193
194
|
|
|
194
195
|
Args:
|
|
195
196
|
repo_path: Path to the repository
|
|
197
|
+
force_full: If True, force full reindex instead of incremental
|
|
198
|
+
verbose: Whether to print progress messages (default: True)
|
|
196
199
|
|
|
197
200
|
Raises:
|
|
198
201
|
Exception: If indexing fails
|
|
199
202
|
"""
|
|
200
203
|
try:
|
|
201
204
|
index_path = get_index_path(repo_path)
|
|
202
|
-
indexer = ElixirIndexer(verbose=
|
|
205
|
+
indexer = ElixirIndexer(verbose=verbose)
|
|
203
206
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
# For better accuracy, install permanently and use cicada-index with --spacy-model medium/large
|
|
207
|
-
indexer.index_repository(
|
|
207
|
+
# Use incremental indexing by default (unless force_full is True)
|
|
208
|
+
indexer.incremental_index_repository(
|
|
208
209
|
repo_path=str(repo_path),
|
|
209
210
|
output_path=str(index_path),
|
|
210
211
|
extract_keywords=True,
|
|
211
|
-
|
|
212
|
+
force_full=force_full,
|
|
212
213
|
)
|
|
213
|
-
|
|
214
|
-
print(f"✓ Repository indexed at {index_path}")
|
|
214
|
+
# Don't print duplicate message - indexer already reports completion
|
|
215
215
|
except Exception as e:
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
if verbose:
|
|
217
|
+
print(f"Error: Failed to index repository: {e}")
|
|
218
|
+
print("Please check that the repository contains valid Elixir files.")
|
|
218
219
|
raise
|
|
219
220
|
|
|
220
221
|
|
|
221
|
-
def
|
|
222
|
+
def setup_multiple_editors(
|
|
223
|
+
editors: list[EditorType],
|
|
224
|
+
repo_path: Path,
|
|
225
|
+
storage_dir: Path,
|
|
226
|
+
verbose: bool = False,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Create MCP configs for multiple editors at once (for server mode).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
editors: List of editor types to configure
|
|
233
|
+
repo_path: Path to the repository
|
|
234
|
+
storage_dir: Path to the storage directory
|
|
235
|
+
verbose: If True, print progress messages
|
|
236
|
+
"""
|
|
237
|
+
for editor in editors:
|
|
238
|
+
try:
|
|
239
|
+
config_path, config_content = get_mcp_config_for_editor(editor, repo_path, storage_dir)
|
|
240
|
+
|
|
241
|
+
# Write config file
|
|
242
|
+
with open(config_path, "w") as f:
|
|
243
|
+
json.dump(config_content, f, indent=2)
|
|
244
|
+
|
|
245
|
+
if verbose:
|
|
246
|
+
print(f"✓ Created {editor.upper()} config at {config_path}")
|
|
247
|
+
except Exception as e:
|
|
248
|
+
if verbose:
|
|
249
|
+
print(f"⚠ Error creating {editor.upper()} config: {e}")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def setup(
|
|
253
|
+
editor: EditorType,
|
|
254
|
+
repo_path: Path | None = None,
|
|
255
|
+
keyword_method: str | None = None,
|
|
256
|
+
keyword_tier: str | None = None,
|
|
257
|
+
index_exists: bool = False,
|
|
258
|
+
) -> None:
|
|
222
259
|
"""
|
|
223
260
|
Run the complete setup for the specified editor.
|
|
224
261
|
|
|
225
262
|
Args:
|
|
226
263
|
editor: Editor type (claude, cursor, vs)
|
|
227
264
|
repo_path: Path to the repository (defaults to current directory)
|
|
265
|
+
keyword_method: Keyword extraction method ('lemminflect' or 'bert'), None for default
|
|
266
|
+
keyword_tier: Model tier ('fast', 'regular', 'max'), None for default
|
|
267
|
+
index_exists: If True, skip banner and show condensed output (index already exists)
|
|
228
268
|
"""
|
|
229
269
|
# Determine repository path
|
|
230
270
|
if repo_path is None:
|
|
231
271
|
repo_path = Path.cwd()
|
|
232
272
|
repo_path = repo_path.resolve()
|
|
233
273
|
|
|
234
|
-
print("=" * 60)
|
|
235
|
-
print(f"Cicada Setup for {editor.upper()}")
|
|
236
|
-
print("=" * 60)
|
|
237
|
-
print()
|
|
238
|
-
|
|
239
274
|
# Create storage directory
|
|
240
|
-
print(f"Repository: {repo_path}")
|
|
241
275
|
storage_dir = create_storage_dir(repo_path)
|
|
242
|
-
print(f"Storage: {storage_dir}")
|
|
243
|
-
print()
|
|
244
276
|
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
# Show condensed output if index already exists
|
|
278
|
+
if index_exists:
|
|
279
|
+
# Determine method and tier for display
|
|
280
|
+
display_method = keyword_method if keyword_method else "lemminflect"
|
|
281
|
+
display_tier = keyword_tier if keyword_tier else "regular"
|
|
282
|
+
print(f"✓ Found existing index ({display_method.upper()} {display_tier})")
|
|
283
|
+
# Skip indexing when index_exists is True - we're just reusing it
|
|
284
|
+
should_index = False
|
|
285
|
+
force_full = False
|
|
286
|
+
# Ensure config.yaml is up to date with current settings
|
|
287
|
+
create_config_yaml(repo_path, storage_dir, keyword_method, keyword_tier, verbose=False)
|
|
288
|
+
else:
|
|
289
|
+
# Show full banner for new setup
|
|
290
|
+
print("=" * 60)
|
|
291
|
+
print(f"Cicada Setup for {editor.upper()}")
|
|
292
|
+
print("=" * 60)
|
|
293
|
+
print()
|
|
294
|
+
print(f"Repository: {repo_path}")
|
|
295
|
+
print(f"Storage: {storage_dir}")
|
|
296
|
+
print()
|
|
248
297
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
298
|
+
# Check if config already exists and determine if we need to reindex
|
|
299
|
+
config_path = get_config_path(repo_path)
|
|
300
|
+
index_path = get_index_path(repo_path)
|
|
301
|
+
should_index = True
|
|
302
|
+
force_full = False
|
|
303
|
+
|
|
304
|
+
if config_path.exists() and index_path.exists():
|
|
305
|
+
import yaml
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
with open(config_path) as f:
|
|
309
|
+
existing_config = yaml.safe_load(f)
|
|
310
|
+
existing_method = existing_config.get("keyword_extraction", {}).get(
|
|
311
|
+
"method", "lemminflect"
|
|
312
|
+
)
|
|
313
|
+
existing_tier = existing_config.get("keyword_extraction", {}).get(
|
|
314
|
+
"tier", "regular"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Determine new method and tier (default to lemminflect/regular if not specified)
|
|
318
|
+
new_method = keyword_method if keyword_method else "lemminflect"
|
|
319
|
+
new_tier = keyword_tier if keyword_tier else "regular"
|
|
320
|
+
|
|
321
|
+
# Check if settings changed
|
|
322
|
+
settings_changed = (existing_method != new_method) or (
|
|
323
|
+
existing_tier != new_tier
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if settings_changed:
|
|
327
|
+
print("=" * 60)
|
|
328
|
+
print("⚠️ WARNING: Index Already Exists")
|
|
329
|
+
print("=" * 60)
|
|
330
|
+
print()
|
|
331
|
+
print(
|
|
332
|
+
f"This repository already has an index with {existing_method.upper()} ({existing_tier}) keyword extraction."
|
|
333
|
+
)
|
|
334
|
+
print(f"You are now switching to {new_method.upper()} ({new_tier}).")
|
|
335
|
+
print()
|
|
336
|
+
print(
|
|
337
|
+
"This will require reindexing the ENTIRE codebase, which may take several minutes."
|
|
338
|
+
)
|
|
339
|
+
print()
|
|
340
|
+
|
|
341
|
+
# Ask for confirmation
|
|
342
|
+
response = input("Do you want to continue? [y/N]: ").strip().lower()
|
|
343
|
+
if response not in ("y", "yes"):
|
|
344
|
+
print("\nSetup cancelled.")
|
|
345
|
+
sys.exit(0)
|
|
346
|
+
print()
|
|
347
|
+
force_full = True # Force full reindex when settings change
|
|
348
|
+
else:
|
|
349
|
+
# Settings unchanged - just use existing index
|
|
350
|
+
print(f"✓ Using existing index ({existing_method}, {existing_tier})")
|
|
351
|
+
print()
|
|
352
|
+
should_index = False
|
|
353
|
+
except Exception:
|
|
354
|
+
# If we can't read the config, just proceed with indexing
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
# Create/update config.yaml BEFORE indexing (indexer reads this to determine keyword method)
|
|
358
|
+
create_config_yaml(repo_path, storage_dir, keyword_method, keyword_tier, verbose=False)
|
|
359
|
+
|
|
360
|
+
# Index repository if needed
|
|
361
|
+
if should_index:
|
|
362
|
+
index_repository(repo_path, force_full=force_full)
|
|
363
|
+
print()
|
|
252
364
|
|
|
253
365
|
# Create MCP config for the editor
|
|
254
|
-
config_path, config_content = get_mcp_config_for_editor(
|
|
255
|
-
|
|
256
|
-
|
|
366
|
+
config_path, config_content = get_mcp_config_for_editor(editor, repo_path, storage_dir)
|
|
367
|
+
|
|
368
|
+
# Check if MCP config already exists
|
|
369
|
+
mcp_config_existed = config_path.exists()
|
|
257
370
|
|
|
258
371
|
# Write config file
|
|
259
372
|
with open(config_path, "w") as f:
|
|
260
373
|
json.dump(config_content, f, indent=2)
|
|
261
374
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
print()
|
|
375
|
+
if index_exists:
|
|
376
|
+
# Show condensed success message
|
|
377
|
+
mcp_verb = "updated" if mcp_config_existed else "created"
|
|
378
|
+
print(f"✓ MCP configuration {mcp_verb} at {config_path}")
|
|
379
|
+
print()
|
|
380
|
+
print(f"Storage: {storage_dir}")
|
|
381
|
+
print()
|
|
382
|
+
print(f"Restart {editor.upper()}.")
|
|
383
|
+
print("To reindex from scratch: cicada clean -f")
|
|
384
|
+
print()
|
|
385
|
+
else:
|
|
386
|
+
# Show simplified success message for first-time setup
|
|
387
|
+
print(f"Project config created at: {config_path}")
|
|
388
|
+
print()
|
|
389
|
+
print(f"Restart {editor.upper()}.")
|
|
390
|
+
print()
|
|
279
391
|
|
|
280
392
|
# Check if running via uvx and suggest permanent installation
|
|
281
393
|
import shutil
|
|
282
|
-
from cicada import __version__
|
|
283
394
|
|
|
284
395
|
# Check for either cicada-mcp or cicada-server (backwards compat)
|
|
285
396
|
if not (shutil.which("cicada-mcp") or shutil.which("cicada-server")):
|
|
286
397
|
print("💡 Tip: For best experience, install Cicada permanently:")
|
|
287
|
-
print(
|
|
288
|
-
f" uv tool install git+https://github.com/wende/cicada.git@v{__version__}"
|
|
289
|
-
)
|
|
398
|
+
print(" uv tool install cicada-mcp")
|
|
290
399
|
print()
|
|
291
400
|
print(" Benefits:")
|
|
292
|
-
print(" • Faster MCP server startup")
|
|
293
|
-
print(" • Access to cicada-index with
|
|
401
|
+
print(" • Faster MCP server startup (no uvx overhead)")
|
|
402
|
+
print(" • Access to cicada-index with enhanced keyword extraction (BERT/lemminflect)")
|
|
294
403
|
print(" • PR indexing with cicada-index-pr")
|
|
295
404
|
print()
|
|
296
405
|
|
|
@@ -299,7 +408,7 @@ def main():
|
|
|
299
408
|
"""Main entry point for the simplified setup script."""
|
|
300
409
|
parser = argparse.ArgumentParser(
|
|
301
410
|
description="Cicada One-Command Setup",
|
|
302
|
-
epilog="Example: uvx cicada claude",
|
|
411
|
+
epilog="Example: uvx --from cicada-mcp cicada claude",
|
|
303
412
|
)
|
|
304
413
|
parser.add_argument(
|
|
305
414
|
"editor",
|
cicada/utils/__init__.py
CHANGED
|
@@ -5,27 +5,27 @@ This package contains shared utilities used across the codebase to reduce
|
|
|
5
5
|
code duplication and improve maintainability.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
8
|
+
from .call_site_formatter import CallSiteFormatter
|
|
9
|
+
from .function_grouper import FunctionGrouper
|
|
10
10
|
from .index_utils import (
|
|
11
11
|
load_index,
|
|
12
|
-
save_index,
|
|
13
12
|
merge_indexes_incremental,
|
|
13
|
+
save_index,
|
|
14
14
|
validate_index_structure,
|
|
15
15
|
)
|
|
16
|
-
from .
|
|
17
|
-
from .call_site_formatter import CallSiteFormatter
|
|
16
|
+
from .path_utils import normalize_file_path, resolve_to_repo_root
|
|
18
17
|
from .signature_builder import SignatureBuilder
|
|
19
|
-
from .text_utils import split_identifier, split_camel_snake_case
|
|
20
18
|
from .storage import (
|
|
21
|
-
get_repo_hash,
|
|
22
|
-
get_storage_dir,
|
|
23
19
|
create_storage_dir,
|
|
24
|
-
get_index_path,
|
|
25
20
|
get_config_path,
|
|
26
21
|
get_hashes_path,
|
|
22
|
+
get_index_path,
|
|
27
23
|
get_pr_index_path,
|
|
24
|
+
get_repo_hash,
|
|
25
|
+
get_storage_dir,
|
|
28
26
|
)
|
|
27
|
+
from .subprocess_runner import SubprocessRunner, run_gh_command, run_git_command
|
|
28
|
+
from .text_utils import split_camel_snake_case, split_identifier
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
31
31
|
"SubprocessRunner",
|
|
@@ -5,7 +5,7 @@ This module provides utilities for grouping and formatting call sites,
|
|
|
5
5
|
eliminating duplication in the formatter module.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class CallSiteFormatter:
|
|
@@ -17,7 +17,7 @@ class CallSiteFormatter:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
@staticmethod
|
|
20
|
-
def group_by_caller(call_sites:
|
|
20
|
+
def group_by_caller(call_sites: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
21
21
|
"""
|
|
22
22
|
Group call sites by their caller (calling_module + calling_function).
|
|
23
23
|
|
|
@@ -56,7 +56,7 @@ class CallSiteFormatter:
|
|
|
56
56
|
# 'lines': [10, 20]
|
|
57
57
|
# }]
|
|
58
58
|
"""
|
|
59
|
-
grouped:
|
|
59
|
+
grouped: dict[tuple, dict[str, Any]] = {}
|
|
60
60
|
|
|
61
61
|
for site in call_sites:
|
|
62
62
|
# Create a key based on caller identity
|
|
@@ -81,9 +81,7 @@ class CallSiteFormatter:
|
|
|
81
81
|
|
|
82
82
|
grouped[key]["lines"].append(site["line"])
|
|
83
83
|
if "code_line" in site:
|
|
84
|
-
grouped[key]["code_lines"].append(
|
|
85
|
-
{"line": site["line"], "code": site["code_line"]}
|
|
86
|
-
)
|
|
84
|
+
grouped[key]["code_lines"].append({"line": site["line"], "code": site["code_line"]})
|
|
87
85
|
|
|
88
86
|
# Convert back to list and sort lines
|
|
89
87
|
result = []
|