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.
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 +122 -107
  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 +103 -209
  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 +189 -87
  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.7.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.7.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.7.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {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 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"]:
@@ -164,7 +145,11 @@ def get_mcp_config_for_editor(
164
145
 
165
146
 
166
147
  def create_config_yaml(
167
- repo_path: Path, storage_dir: Path, verbose: bool = True
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
- verbose: Whether to print progress messages (default: True)
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
- # Index with keyword extraction enabled by default
210
- # Note: Using 'small' model for compatibility with uvx
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
- spacy_model="small",
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 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:
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
- # Index repository
253
- index_repository(repo_path)
254
- 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()
255
297
 
256
- # Create config.yaml
257
- create_config_yaml(repo_path, storage_dir)
258
- 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()
259
364
 
260
365
  # Create MCP config for the editor
261
- config_path, config_content = get_mcp_config_for_editor(
262
- editor, repo_path, storage_dir
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
- print(f"✓ MCP configuration created at {config_path}")
270
- print()
271
-
272
- print("=" * 60)
273
- print("✓ Setup Complete!")
274
- print("=" * 60)
275
- print()
276
- print("Next steps:")
277
- print(f"1. Restart {editor.upper()}")
278
- print("2. Cicada MCP server will be available automatically")
279
- print()
280
- print("Storage location:")
281
- print(f" {storage_dir}")
282
- print()
283
- print("All index files are stored outside your repository.")
284
- print(f"Only {config_path.name} was added to your repo.")
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 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)")
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 .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 = []
@@ -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 Dict, List, Any, Tuple
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: List[Dict[str, Any]],
22
- ) -> Dict[Tuple[str, int], List[Dict[str, Any]]]:
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: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
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"])