cicada-mcp 0.1.7__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 +122 -107
- 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 +103 -209
- 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 +189 -87
- 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.7.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.7.dist-info/RECORD +0 -47
- cicada_mcp-0.1.7.dist-info/entry_points.txt +0 -9
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.1.7.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"]:
|
|
@@ -164,7 +145,11 @@ def get_mcp_config_for_editor(
|
|
|
164
145
|
|
|
165
146
|
|
|
166
147
|
def create_config_yaml(
|
|
167
|
-
repo_path: Path,
|
|
148
|
+
repo_path: Path,
|
|
149
|
+
storage_dir: Path,
|
|
150
|
+
keyword_method: str | None = None,
|
|
151
|
+
keyword_tier: str | None = None,
|
|
152
|
+
verbose: bool = True,
|
|
168
153
|
) -> None:
|
|
169
154
|
"""
|
|
170
155
|
Create config.yaml in storage directory.
|
|
@@ -172,16 +157,28 @@ def create_config_yaml(
|
|
|
172
157
|
Args:
|
|
173
158
|
repo_path: Path to the repository
|
|
174
159
|
storage_dir: Path to the storage directory
|
|
175
|
-
|
|
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.
|
|
176
163
|
"""
|
|
177
164
|
config_path = get_config_path(repo_path)
|
|
178
165
|
index_path = get_index_path(repo_path)
|
|
179
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
|
+
|
|
180
173
|
config_content = f"""repository:
|
|
181
174
|
path: {repo_path}
|
|
182
175
|
|
|
183
176
|
storage:
|
|
184
177
|
index_path: {index_path}
|
|
178
|
+
|
|
179
|
+
keyword_extraction:
|
|
180
|
+
method: {keyword_method}
|
|
181
|
+
tier: {keyword_tier}
|
|
185
182
|
"""
|
|
186
183
|
|
|
187
184
|
with open(config_path, "w") as f:
|
|
@@ -191,12 +188,13 @@ storage:
|
|
|
191
188
|
print(f"✓ Config file created at {config_path}")
|
|
192
189
|
|
|
193
190
|
|
|
194
|
-
def index_repository(repo_path: Path, verbose: bool = True) -> None:
|
|
191
|
+
def index_repository(repo_path: Path, force_full: bool = False, verbose: bool = True) -> None:
|
|
195
192
|
"""
|
|
196
193
|
Index the repository with keyword extraction enabled.
|
|
197
194
|
|
|
198
195
|
Args:
|
|
199
196
|
repo_path: Path to the repository
|
|
197
|
+
force_full: If True, force full reindex instead of incremental
|
|
200
198
|
verbose: Whether to print progress messages (default: True)
|
|
201
199
|
|
|
202
200
|
Raises:
|
|
@@ -206,18 +204,14 @@ def index_repository(repo_path: Path, verbose: bool = True) -> None:
|
|
|
206
204
|
index_path = get_index_path(repo_path)
|
|
207
205
|
indexer = ElixirIndexer(verbose=verbose)
|
|
208
206
|
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
# For better accuracy, install permanently and use cicada-index with --spacy-model medium/large
|
|
212
|
-
indexer.index_repository(
|
|
207
|
+
# Use incremental indexing by default (unless force_full is True)
|
|
208
|
+
indexer.incremental_index_repository(
|
|
213
209
|
repo_path=str(repo_path),
|
|
214
210
|
output_path=str(index_path),
|
|
215
211
|
extract_keywords=True,
|
|
216
|
-
|
|
212
|
+
force_full=force_full,
|
|
217
213
|
)
|
|
218
|
-
|
|
219
|
-
if verbose:
|
|
220
|
-
print(f"✓ Repository indexed at {index_path}")
|
|
214
|
+
# Don't print duplicate message - indexer already reports completion
|
|
221
215
|
except Exception as e:
|
|
222
216
|
if verbose:
|
|
223
217
|
print(f"Error: Failed to index repository: {e}")
|
|
@@ -225,79 +219,187 @@ def index_repository(repo_path: Path, verbose: bool = True) -> None:
|
|
|
225
219
|
raise
|
|
226
220
|
|
|
227
221
|
|
|
228
|
-
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:
|
|
229
259
|
"""
|
|
230
260
|
Run the complete setup for the specified editor.
|
|
231
261
|
|
|
232
262
|
Args:
|
|
233
263
|
editor: Editor type (claude, cursor, vs)
|
|
234
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)
|
|
235
268
|
"""
|
|
236
269
|
# Determine repository path
|
|
237
270
|
if repo_path is None:
|
|
238
271
|
repo_path = Path.cwd()
|
|
239
272
|
repo_path = repo_path.resolve()
|
|
240
273
|
|
|
241
|
-
print("=" * 60)
|
|
242
|
-
print(f"Cicada Setup for {editor.upper()}")
|
|
243
|
-
print("=" * 60)
|
|
244
|
-
print()
|
|
245
|
-
|
|
246
274
|
# Create storage directory
|
|
247
|
-
print(f"Repository: {repo_path}")
|
|
248
275
|
storage_dir = create_storage_dir(repo_path)
|
|
249
|
-
print(f"Storage: {storage_dir}")
|
|
250
|
-
print()
|
|
251
276
|
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
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()
|
|
255
297
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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()
|
|
259
364
|
|
|
260
365
|
# Create MCP config for the editor
|
|
261
|
-
config_path, config_content = get_mcp_config_for_editor(
|
|
262
|
-
|
|
263
|
-
|
|
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()
|
|
264
370
|
|
|
265
371
|
# Write config file
|
|
266
372
|
with open(config_path, "w") as f:
|
|
267
373
|
json.dump(config_content, f, indent=2)
|
|
268
374
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
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()
|
|
286
391
|
|
|
287
392
|
# Check if running via uvx and suggest permanent installation
|
|
288
393
|
import shutil
|
|
289
|
-
from cicada import __version__
|
|
290
394
|
|
|
291
395
|
# Check for either cicada-mcp or cicada-server (backwards compat)
|
|
292
396
|
if not (shutil.which("cicada-mcp") or shutil.which("cicada-server")):
|
|
293
397
|
print("💡 Tip: For best experience, install Cicada permanently:")
|
|
294
|
-
print(
|
|
295
|
-
f" uv tool install git+https://github.com/wende/cicada.git@v{__version__}"
|
|
296
|
-
)
|
|
398
|
+
print(" uv tool install cicada-mcp")
|
|
297
399
|
print()
|
|
298
400
|
print(" Benefits:")
|
|
299
|
-
print(" • Faster MCP server startup")
|
|
300
|
-
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)")
|
|
301
403
|
print(" • PR indexing with cicada-index-pr")
|
|
302
404
|
print()
|
|
303
405
|
|
|
@@ -306,7 +408,7 @@ def main():
|
|
|
306
408
|
"""Main entry point for the simplified setup script."""
|
|
307
409
|
parser = argparse.ArgumentParser(
|
|
308
410
|
description="Cicada One-Command Setup",
|
|
309
|
-
epilog="Example: uvx cicada claude",
|
|
411
|
+
epilog="Example: uvx --from cicada-mcp cicada claude",
|
|
310
412
|
)
|
|
311
413
|
parser.add_argument(
|
|
312
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 = []
|
cicada/utils/function_grouper.py
CHANGED
|
@@ -5,7 +5,7 @@ This module provides utilities for grouping functions by name and arity,
|
|
|
5
5
|
eliminating duplication across formatter and other modules.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class FunctionGrouper:
|
|
@@ -18,8 +18,8 @@ class FunctionGrouper:
|
|
|
18
18
|
|
|
19
19
|
@staticmethod
|
|
20
20
|
def group_by_name_arity(
|
|
21
|
-
functions:
|
|
22
|
-
) ->
|
|
21
|
+
functions: list[dict[str, Any]],
|
|
22
|
+
) -> dict[tuple[str, int], list[dict[str, Any]]]:
|
|
23
23
|
"""
|
|
24
24
|
Group functions by their (name, arity) tuple.
|
|
25
25
|
|
|
@@ -46,7 +46,7 @@ class FunctionGrouper:
|
|
|
46
46
|
# ('create', 2): [{'name': 'create', 'arity': 2, ...}]
|
|
47
47
|
# }
|
|
48
48
|
"""
|
|
49
|
-
grouped:
|
|
49
|
+
grouped: dict[tuple[str, int], list[dict[str, Any]]] = {}
|
|
50
50
|
|
|
51
51
|
for func in functions:
|
|
52
52
|
key = (func["name"], func["arity"])
|