cicada-mcp 0.1.5__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. cicada/ascii_art.py +60 -0
  2. cicada/clean.py +195 -60
  3. cicada/cli.py +757 -0
  4. cicada/colors.py +27 -0
  5. cicada/command_logger.py +14 -16
  6. cicada/dead_code_analyzer.py +12 -19
  7. cicada/extractors/__init__.py +6 -6
  8. cicada/extractors/base.py +3 -3
  9. cicada/extractors/call.py +11 -15
  10. cicada/extractors/dependency.py +39 -51
  11. cicada/extractors/doc.py +8 -9
  12. cicada/extractors/function.py +12 -24
  13. cicada/extractors/module.py +11 -15
  14. cicada/extractors/spec.py +8 -12
  15. cicada/find_dead_code.py +15 -39
  16. cicada/formatter.py +37 -91
  17. cicada/git_helper.py +22 -34
  18. cicada/indexer.py +165 -132
  19. cicada/interactive_setup.py +490 -0
  20. cicada/keybert_extractor.py +286 -0
  21. cicada/keyword_search.py +22 -30
  22. cicada/keyword_test.py +127 -0
  23. cicada/lightweight_keyword_extractor.py +5 -13
  24. cicada/mcp_entry.py +683 -0
  25. cicada/mcp_server.py +110 -232
  26. cicada/parser.py +9 -9
  27. cicada/pr_finder.py +15 -19
  28. cicada/pr_indexer/__init__.py +3 -3
  29. cicada/pr_indexer/cli.py +4 -9
  30. cicada/pr_indexer/github_api_client.py +22 -37
  31. cicada/pr_indexer/indexer.py +17 -29
  32. cicada/pr_indexer/line_mapper.py +8 -12
  33. cicada/pr_indexer/pr_index_builder.py +22 -34
  34. cicada/setup.py +198 -89
  35. cicada/utils/__init__.py +9 -9
  36. cicada/utils/call_site_formatter.py +4 -6
  37. cicada/utils/function_grouper.py +4 -4
  38. cicada/utils/hash_utils.py +12 -15
  39. cicada/utils/index_utils.py +15 -15
  40. cicada/utils/path_utils.py +24 -29
  41. cicada/utils/signature_builder.py +3 -3
  42. cicada/utils/subprocess_runner.py +17 -19
  43. cicada/utils/text_utils.py +1 -2
  44. cicada/version_check.py +2 -5
  45. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
  46. cicada_mcp-0.2.0.dist-info/RECORD +53 -0
  47. cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  48. cicada/install.py +0 -741
  49. cicada_mcp-0.1.5.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
@@ -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 Dict, List, Any, Optional
9
+ from typing import Any
10
10
 
11
- from cicada.utils import load_index as load_index_util, save_index as save_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: List[Dict[str, Any]], preserve_last_pr: Optional[int] = None
35
- ) -> Dict[str, Any]:
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: Dict[str, Any] = {
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: Dict[str, Any] = {
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: List[Dict[str, Any]]) -> Dict[str, int]:
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: List[Dict[str, Any]]) -> Dict[str, List[int]]:
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: Dict[str, Any], new_prs: List[Dict[str, Any]]
132
- ) -> Dict[str, Any]:
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"].keys()]
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: Dict[str, Any], partial_index: Dict[str, Any]
180
- ) -> Dict[str, Any]:
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) -> Optional[Dict[str, Any]]:
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: Dict[str, Any], output_path: str) -> None:
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
- get_hashes_path,
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, "r") as f:
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 IOError as e:
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
- # Detect installation method
103
- import shutil
104
-
105
- # Check for cicada-mcp first (new name), fall back to cicada-server (backwards compat)
106
- has_cicada_mcp = shutil.which("cicada-mcp") is not None
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(repo_path: Path, storage_dir: Path) -> None:
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
- print(f"✓ Config file created at {config_path}")
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=True)
205
+ indexer = ElixirIndexer(verbose=verbose)
203
206
 
204
- # Index with keyword extraction enabled by default
205
- # Note: Using 'small' model for compatibility with uvx
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
- spacy_model="small",
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
- print(f"Error: Failed to index repository: {e}")
217
- print("Please check that the repository contains valid Elixir files.")
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 setup(editor: EditorType, repo_path: Path | None = None) -> None:
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
- # Index repository
246
- index_repository(repo_path)
247
- print()
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
- # Create config.yaml
250
- create_config_yaml(repo_path, storage_dir)
251
- print()
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
- editor, repo_path, storage_dir
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
- print(f"✓ MCP configuration created at {config_path}")
263
- print()
264
-
265
- print("=" * 60)
266
- print("✓ Setup Complete!")
267
- print("=" * 60)
268
- print()
269
- print("Next steps:")
270
- print(f"1. Restart {editor.upper()}")
271
- print("2. Cicada MCP server will be available automatically")
272
- print()
273
- print("Storage location:")
274
- print(f" {storage_dir}")
275
- print()
276
- print("All index files are stored outside your repository.")
277
- print(f"Only {config_path.name} was added to your repo.")
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 medium/large spaCy models")
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 .subprocess_runner import SubprocessRunner, run_git_command, run_gh_command
9
- from .path_utils import normalize_file_path, resolve_to_repo_root
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 .function_grouper import FunctionGrouper
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 Dict, List, Any, Tuple
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: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
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: Dict[Tuple, Dict[str, Any]] = {}
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 = []