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
cicada/ascii_art.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""ASCII art and banner generation for Cicada CLI."""
|
|
2
|
+
|
|
3
|
+
from cicada.colors import CYAN, RESET, YELLOW
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_gradient_ascii_art():
|
|
7
|
+
"""Generate cicada ASCII art with gradient colors from #E5C890 to #D17958."""
|
|
8
|
+
start_hex = "E5C890"
|
|
9
|
+
end_hex = "D17958"
|
|
10
|
+
|
|
11
|
+
# Parse hex colors to RGB
|
|
12
|
+
start_rgb = tuple(int(start_hex[i : i + 2], 16) for i in (0, 2, 4))
|
|
13
|
+
end_rgb = tuple(int(end_hex[i : i + 2], 16) for i in (0, 2, 4))
|
|
14
|
+
|
|
15
|
+
# ASCII art lines - explicitly defined to preserve all leading spaces
|
|
16
|
+
lines = [
|
|
17
|
+
' :., . _.: "',
|
|
18
|
+
" > =.t@_. . . ._ j@F:++<",
|
|
19
|
+
" .a??_'dB_ a_ . > \" < . _: _Ba _??p",
|
|
20
|
+
' ` \'_\'--m."=o.. , @|D,, ..=+".&--",,.',
|
|
21
|
+
" `.=mm=~\"'_. .+===+. \"'~=mm=",
|
|
22
|
+
" ..-_. '-. g_._g .-' . .",
|
|
23
|
+
" '.\" , mgggm, , \".'",
|
|
24
|
+
' /! "BBB" !\\',
|
|
25
|
+
" / " + "'" + '"""' + "' \\",
|
|
26
|
+
' "',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
num_lines = len(lines)
|
|
30
|
+
result_lines = []
|
|
31
|
+
|
|
32
|
+
for i, line in enumerate(lines):
|
|
33
|
+
# Calculate interpolation factor (0 to 1)
|
|
34
|
+
t = i / (num_lines - 1) if num_lines > 1 else 0
|
|
35
|
+
|
|
36
|
+
# Interpolate RGB values
|
|
37
|
+
r = int(start_rgb[0] + (end_rgb[0] - start_rgb[0]) * t)
|
|
38
|
+
g = int(start_rgb[1] + (end_rgb[1] - start_rgb[1]) * t)
|
|
39
|
+
b = int(start_rgb[2] + (end_rgb[2] - start_rgb[2]) * t)
|
|
40
|
+
|
|
41
|
+
# Create ANSI color code for this line
|
|
42
|
+
color_code = f"\033[38;2;{r};{g};{b}m"
|
|
43
|
+
# Cast to str to satisfy type checker's LiteralString requirement
|
|
44
|
+
colored_line: str = str(color_code + line + "\033[0m")
|
|
45
|
+
result_lines.append(colored_line) # type: ignore[arg-type]
|
|
46
|
+
|
|
47
|
+
return "\n" + "\n".join(result_lines) + "\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Pre-generate the ASCII art banner
|
|
51
|
+
CICADA_ASCII_ART = generate_gradient_ascii_art()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_welcome_banner():
|
|
55
|
+
"""Generate complete welcome banner with ASCII art and welcome message."""
|
|
56
|
+
banner = CICADA_ASCII_ART
|
|
57
|
+
banner += f"{CYAN}{'=' * 66}{RESET}\n"
|
|
58
|
+
banner += f"{YELLOW} Welcome to CICADA - Elixir Code Intelligence{RESET}\n"
|
|
59
|
+
banner += f"{CYAN}{'=' * 66}{RESET}\n"
|
|
60
|
+
return banner
|
cicada/clean.py
CHANGED
|
@@ -9,9 +9,24 @@ import argparse
|
|
|
9
9
|
import json
|
|
10
10
|
import shutil
|
|
11
11
|
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
|
-
from cicada.utils import
|
|
15
|
+
from cicada.utils import (
|
|
16
|
+
get_hashes_path,
|
|
17
|
+
get_index_path,
|
|
18
|
+
get_pr_index_path,
|
|
19
|
+
get_storage_dir,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CleanItem:
|
|
25
|
+
"""Represents an item to be cleaned/removed."""
|
|
26
|
+
|
|
27
|
+
description: str
|
|
28
|
+
path: Path
|
|
29
|
+
is_mcp_config: bool = False
|
|
15
30
|
|
|
16
31
|
|
|
17
32
|
def remove_mcp_config_entry(config_path: Path, server_key: str = "cicada") -> bool:
|
|
@@ -29,7 +44,7 @@ def remove_mcp_config_entry(config_path: Path, server_key: str = "cicada") -> bo
|
|
|
29
44
|
return False
|
|
30
45
|
|
|
31
46
|
try:
|
|
32
|
-
with open(config_path
|
|
47
|
+
with open(config_path) as f:
|
|
33
48
|
config = json.load(f)
|
|
34
49
|
|
|
35
50
|
# Determine the config key based on editor type
|
|
@@ -50,12 +65,121 @@ def remove_mcp_config_entry(config_path: Path, server_key: str = "cicada") -> bo
|
|
|
50
65
|
|
|
51
66
|
return True
|
|
52
67
|
|
|
53
|
-
except (json.JSONDecodeError
|
|
68
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
54
69
|
print(f"Warning: Could not process {config_path}: {e}")
|
|
55
70
|
|
|
56
71
|
return False
|
|
57
72
|
|
|
58
73
|
|
|
74
|
+
def clean_index_only(repo_path: Path) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Remove only the main index files (index.json and hashes.json).
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
repo_path: Path to the repository
|
|
80
|
+
"""
|
|
81
|
+
repo_path = repo_path.resolve()
|
|
82
|
+
|
|
83
|
+
print("=" * 60)
|
|
84
|
+
print("Cicada Clean - Main Index")
|
|
85
|
+
print("=" * 60)
|
|
86
|
+
print()
|
|
87
|
+
print(f"Repository: {repo_path}")
|
|
88
|
+
print()
|
|
89
|
+
|
|
90
|
+
# Collect index files to remove
|
|
91
|
+
items_to_remove: list[CleanItem] = []
|
|
92
|
+
|
|
93
|
+
index_path = get_index_path(repo_path)
|
|
94
|
+
hashes_path = get_hashes_path(repo_path)
|
|
95
|
+
|
|
96
|
+
if index_path.exists():
|
|
97
|
+
items_to_remove.append(CleanItem("Main index", index_path))
|
|
98
|
+
if hashes_path.exists():
|
|
99
|
+
items_to_remove.append(CleanItem("File hashes", hashes_path))
|
|
100
|
+
|
|
101
|
+
# Show what will be removed
|
|
102
|
+
if not items_to_remove:
|
|
103
|
+
print("✓ No main index files found.")
|
|
104
|
+
print()
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
print("The following items will be removed:")
|
|
108
|
+
print()
|
|
109
|
+
for item in items_to_remove:
|
|
110
|
+
print(f" • {item.description}: {item.path}")
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
# Remove items
|
|
114
|
+
removed_count = 0
|
|
115
|
+
errors = []
|
|
116
|
+
for item in items_to_remove:
|
|
117
|
+
try:
|
|
118
|
+
item.path.unlink()
|
|
119
|
+
print(f"✓ Removed {item.description}")
|
|
120
|
+
removed_count += 1
|
|
121
|
+
except (OSError, PermissionError) as e:
|
|
122
|
+
error_msg = f"Failed to remove {item.description}: {e}"
|
|
123
|
+
print(f"✗ {error_msg}")
|
|
124
|
+
errors.append(error_msg)
|
|
125
|
+
|
|
126
|
+
print()
|
|
127
|
+
print("=" * 60)
|
|
128
|
+
if errors:
|
|
129
|
+
print(
|
|
130
|
+
f"⚠ Cleanup completed with errors ({removed_count}/{len(items_to_remove)} items removed)"
|
|
131
|
+
)
|
|
132
|
+
print("=" * 60)
|
|
133
|
+
print()
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
else:
|
|
136
|
+
print(f"✓ Cleanup Complete! ({removed_count} items removed)")
|
|
137
|
+
print("=" * 60)
|
|
138
|
+
print()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def clean_pr_index_only(repo_path: Path) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Remove only the PR index file (pr_index.json).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
repo_path: Path to the repository
|
|
147
|
+
"""
|
|
148
|
+
repo_path = repo_path.resolve()
|
|
149
|
+
|
|
150
|
+
print("=" * 60)
|
|
151
|
+
print("Cicada Clean - PR Index")
|
|
152
|
+
print("=" * 60)
|
|
153
|
+
print()
|
|
154
|
+
print(f"Repository: {repo_path}")
|
|
155
|
+
print()
|
|
156
|
+
|
|
157
|
+
pr_index_path = get_pr_index_path(repo_path)
|
|
158
|
+
|
|
159
|
+
if not pr_index_path.exists():
|
|
160
|
+
print("✓ No PR index file found.")
|
|
161
|
+
print()
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
print("The following item will be removed:")
|
|
165
|
+
print()
|
|
166
|
+
print(f" • PR index: {pr_index_path}")
|
|
167
|
+
print()
|
|
168
|
+
|
|
169
|
+
# Remove PR index
|
|
170
|
+
try:
|
|
171
|
+
pr_index_path.unlink()
|
|
172
|
+
print("✓ Removed PR index")
|
|
173
|
+
print()
|
|
174
|
+
print("=" * 60)
|
|
175
|
+
print("✓ Cleanup Complete!")
|
|
176
|
+
print("=" * 60)
|
|
177
|
+
print()
|
|
178
|
+
except (OSError, PermissionError) as e:
|
|
179
|
+
print(f"✗ Failed to remove PR index: {e}")
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
|
|
59
183
|
def clean_repository(repo_path: Path, force: bool = False) -> None:
|
|
60
184
|
"""
|
|
61
185
|
Remove all Cicada configuration and indexes for a repository.
|
|
@@ -74,17 +198,17 @@ def clean_repository(repo_path: Path, force: bool = False) -> None:
|
|
|
74
198
|
print()
|
|
75
199
|
|
|
76
200
|
# Collect items to remove
|
|
77
|
-
items_to_remove = []
|
|
201
|
+
items_to_remove: list[CleanItem] = []
|
|
78
202
|
|
|
79
203
|
# 1. Storage directory (~/.cicada/projects/<repo_hash>/)
|
|
80
204
|
storage_dir = get_storage_dir(repo_path)
|
|
81
205
|
if storage_dir.exists():
|
|
82
|
-
items_to_remove.append(("Storage directory", storage_dir))
|
|
206
|
+
items_to_remove.append(CleanItem("Storage directory", storage_dir))
|
|
83
207
|
|
|
84
208
|
# 2. Old .cicada directory (backward compatibility)
|
|
85
209
|
old_cicada_dir = repo_path / ".cicada"
|
|
86
210
|
if old_cicada_dir.exists():
|
|
87
|
-
items_to_remove.append(("Legacy .cicada directory", old_cicada_dir))
|
|
211
|
+
items_to_remove.append(CleanItem("Legacy .cicada directory", old_cicada_dir))
|
|
88
212
|
|
|
89
213
|
# 3. MCP config files
|
|
90
214
|
mcp_configs = [
|
|
@@ -97,18 +221,14 @@ def clean_repository(repo_path: Path, force: bool = False) -> None:
|
|
|
97
221
|
if config_path.exists():
|
|
98
222
|
# Check if cicada entry exists
|
|
99
223
|
try:
|
|
100
|
-
with open(config_path
|
|
224
|
+
with open(config_path) as f:
|
|
101
225
|
config = json.load(f)
|
|
102
226
|
|
|
103
|
-
config_key = (
|
|
104
|
-
"mcpServers" if ".vscode" not in str(config_path) else "mcp.servers"
|
|
105
|
-
)
|
|
227
|
+
config_key = "mcpServers" if ".vscode" not in str(config_path) else "mcp.servers"
|
|
106
228
|
|
|
107
229
|
if config_key in config and "cicada" in config[config_key]:
|
|
108
|
-
items_to_remove.append(
|
|
109
|
-
|
|
110
|
-
) # True = is MCP config
|
|
111
|
-
except (json.JSONDecodeError, IOError):
|
|
230
|
+
items_to_remove.append(CleanItem(desc, config_path, is_mcp_config=True))
|
|
231
|
+
except (OSError, json.JSONDecodeError):
|
|
112
232
|
pass
|
|
113
233
|
|
|
114
234
|
# Show what will be removed
|
|
@@ -120,10 +240,10 @@ def clean_repository(repo_path: Path, force: bool = False) -> None:
|
|
|
120
240
|
print("The following items will be removed:")
|
|
121
241
|
print()
|
|
122
242
|
for item in items_to_remove:
|
|
123
|
-
if
|
|
124
|
-
print(f" • {item
|
|
243
|
+
if item.is_mcp_config:
|
|
244
|
+
print(f" • {item.description}: Remove 'cicada' entry from {item.path}")
|
|
125
245
|
else:
|
|
126
|
-
print(f" • {item
|
|
246
|
+
print(f" • {item.description}: {item.path}")
|
|
127
247
|
print()
|
|
128
248
|
|
|
129
249
|
# Confirmation prompt
|
|
@@ -139,33 +259,46 @@ def clean_repository(repo_path: Path, force: bool = False) -> None:
|
|
|
139
259
|
|
|
140
260
|
# Remove items
|
|
141
261
|
removed_count = 0
|
|
262
|
+
errors = []
|
|
142
263
|
for item in items_to_remove:
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
print(f"✓ Removed 'cicada' entry from {desc}")
|
|
264
|
+
if item.is_mcp_config:
|
|
265
|
+
if remove_mcp_config_entry(item.path):
|
|
266
|
+
print(f"✓ Removed 'cicada' entry from {item.description}")
|
|
147
267
|
removed_count += 1
|
|
268
|
+
else:
|
|
269
|
+
error_msg = f"Failed to remove 'cicada' entry from {item.description}"
|
|
270
|
+
print(f"✗ {error_msg}")
|
|
271
|
+
errors.append(error_msg)
|
|
148
272
|
else:
|
|
149
|
-
desc, path = item
|
|
150
273
|
try:
|
|
151
|
-
if path.is_dir():
|
|
152
|
-
shutil.rmtree(path)
|
|
274
|
+
if item.path.is_dir():
|
|
275
|
+
shutil.rmtree(item.path)
|
|
153
276
|
else:
|
|
154
|
-
path.unlink()
|
|
155
|
-
print(f"✓ Removed {
|
|
277
|
+
item.path.unlink()
|
|
278
|
+
print(f"✓ Removed {item.description}")
|
|
156
279
|
removed_count += 1
|
|
157
280
|
except (OSError, PermissionError) as e:
|
|
158
|
-
|
|
281
|
+
error_msg = f"Failed to remove {item.description}: {e}"
|
|
282
|
+
print(f"✗ {error_msg}")
|
|
283
|
+
errors.append(error_msg)
|
|
159
284
|
|
|
160
285
|
print()
|
|
161
286
|
print("=" * 60)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
287
|
+
if errors:
|
|
288
|
+
print(
|
|
289
|
+
f"⚠ Cleanup completed with errors ({removed_count}/{len(items_to_remove)} items removed)"
|
|
290
|
+
)
|
|
291
|
+
print("=" * 60)
|
|
292
|
+
print()
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
else:
|
|
295
|
+
print(f"✓ Cleanup Complete! ({removed_count} items removed)")
|
|
296
|
+
print("=" * 60)
|
|
297
|
+
print()
|
|
298
|
+
print("Next steps:")
|
|
299
|
+
print("1. Restart your editor if it's currently running")
|
|
300
|
+
print("2. Run 'uvx cicada <editor>' to set up Cicada again")
|
|
301
|
+
print()
|
|
169
302
|
|
|
170
303
|
|
|
171
304
|
def clean_all_projects(force: bool = False) -> None:
|
|
@@ -219,36 +352,48 @@ def clean_all_projects(force: bool = False) -> None:
|
|
|
219
352
|
|
|
220
353
|
# Remove all project directories
|
|
221
354
|
removed_count = 0
|
|
355
|
+
errors = []
|
|
222
356
|
for proj_dir in project_dirs:
|
|
223
357
|
try:
|
|
224
358
|
shutil.rmtree(proj_dir)
|
|
225
359
|
print(f"✓ Removed {proj_dir.name}/")
|
|
226
360
|
removed_count += 1
|
|
227
361
|
except (OSError, PermissionError) as e:
|
|
228
|
-
|
|
362
|
+
error_msg = f"Failed to remove {proj_dir.name}/: {e}"
|
|
363
|
+
print(f"✗ {error_msg}")
|
|
364
|
+
errors.append(error_msg)
|
|
229
365
|
|
|
230
366
|
print()
|
|
231
367
|
print("=" * 60)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
368
|
+
if errors:
|
|
369
|
+
print(
|
|
370
|
+
f"⚠ Cleanup completed with errors ({removed_count}/{len(project_dirs)} projects removed)"
|
|
371
|
+
)
|
|
372
|
+
print("=" * 60)
|
|
373
|
+
print()
|
|
374
|
+
sys.exit(1)
|
|
375
|
+
else:
|
|
376
|
+
print(f"✓ Cleanup Complete! ({removed_count}/{len(project_dirs)} projects removed)")
|
|
377
|
+
print("=" * 60)
|
|
378
|
+
print()
|
|
235
379
|
|
|
236
380
|
|
|
237
381
|
def main():
|
|
238
|
-
"""
|
|
382
|
+
"""
|
|
383
|
+
Main entry point for the clean command.
|
|
384
|
+
|
|
385
|
+
Note: This function is kept for backward compatibility but the unified CLI
|
|
386
|
+
in cli.py should be used instead (cicada clean).
|
|
387
|
+
"""
|
|
239
388
|
parser = argparse.ArgumentParser(
|
|
240
|
-
description="Remove all Cicada configuration and indexes for
|
|
389
|
+
description="Remove all Cicada configuration and indexes for current repository",
|
|
241
390
|
epilog="Examples:\n"
|
|
242
|
-
" cicada
|
|
243
|
-
" cicada
|
|
391
|
+
" cicada clean # Clean current repository\n"
|
|
392
|
+
" cicada clean -f # Clean current repository (skip confirmation)\n"
|
|
393
|
+
" cicada clean --all # Remove ALL project storage\n"
|
|
394
|
+
" cicada clean --all -f # Remove ALL project storage (skip confirmation)\n",
|
|
244
395
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
245
396
|
)
|
|
246
|
-
parser.add_argument(
|
|
247
|
-
"repo",
|
|
248
|
-
nargs="?",
|
|
249
|
-
default=None,
|
|
250
|
-
help="Path to the repository (default: current directory)",
|
|
251
|
-
)
|
|
252
397
|
parser.add_argument(
|
|
253
398
|
"-f",
|
|
254
399
|
"--force",
|
|
@@ -272,18 +417,8 @@ def main():
|
|
|
272
417
|
sys.exit(1)
|
|
273
418
|
return
|
|
274
419
|
|
|
275
|
-
#
|
|
276
|
-
repo_path = Path
|
|
277
|
-
|
|
278
|
-
# Validate path exists
|
|
279
|
-
if not repo_path.exists():
|
|
280
|
-
print(f"Error: Path does not exist: {repo_path}")
|
|
281
|
-
sys.exit(1)
|
|
282
|
-
|
|
283
|
-
# Validate path is a directory
|
|
284
|
-
if not repo_path.is_dir():
|
|
285
|
-
print(f"Error: Path is not a directory: {repo_path}")
|
|
286
|
-
sys.exit(1)
|
|
420
|
+
# Clean current directory
|
|
421
|
+
repo_path = Path.cwd()
|
|
287
422
|
|
|
288
423
|
# Run cleanup
|
|
289
424
|
try:
|