mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__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 (92) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +111 -0
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +74 -0
  7. mcp_vector_search/analysis/collectors/base.py +164 -0
  8. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  9. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  10. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  11. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  12. mcp_vector_search/analysis/collectors/smells.py +325 -0
  13. mcp_vector_search/analysis/debt.py +516 -0
  14. mcp_vector_search/analysis/interpretation.py +685 -0
  15. mcp_vector_search/analysis/metrics.py +414 -0
  16. mcp_vector_search/analysis/reporters/__init__.py +7 -0
  17. mcp_vector_search/analysis/reporters/console.py +646 -0
  18. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  19. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  20. mcp_vector_search/analysis/storage/__init__.py +93 -0
  21. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  22. mcp_vector_search/analysis/storage/schema.py +245 -0
  23. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  24. mcp_vector_search/analysis/trends.py +308 -0
  25. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  26. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  27. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  28. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  29. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  30. mcp_vector_search/cli/commands/analyze.py +1062 -0
  31. mcp_vector_search/cli/commands/chat.py +1455 -0
  32. mcp_vector_search/cli/commands/index.py +621 -5
  33. mcp_vector_search/cli/commands/index_background.py +467 -0
  34. mcp_vector_search/cli/commands/init.py +13 -0
  35. mcp_vector_search/cli/commands/install.py +597 -335
  36. mcp_vector_search/cli/commands/install_old.py +8 -4
  37. mcp_vector_search/cli/commands/mcp.py +78 -6
  38. mcp_vector_search/cli/commands/reset.py +68 -26
  39. mcp_vector_search/cli/commands/search.py +224 -8
  40. mcp_vector_search/cli/commands/setup.py +1184 -0
  41. mcp_vector_search/cli/commands/status.py +339 -5
  42. mcp_vector_search/cli/commands/uninstall.py +276 -357
  43. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  44. mcp_vector_search/cli/commands/visualize/cli.py +292 -0
  45. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  46. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  47. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
  48. mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
  49. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  50. mcp_vector_search/cli/commands/visualize/server.py +600 -0
  51. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  52. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  53. mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
  54. mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
  55. mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
  56. mcp_vector_search/cli/didyoumean.py +27 -2
  57. mcp_vector_search/cli/main.py +127 -160
  58. mcp_vector_search/cli/output.py +158 -13
  59. mcp_vector_search/config/__init__.py +4 -0
  60. mcp_vector_search/config/default_thresholds.yaml +52 -0
  61. mcp_vector_search/config/settings.py +12 -0
  62. mcp_vector_search/config/thresholds.py +273 -0
  63. mcp_vector_search/core/__init__.py +16 -0
  64. mcp_vector_search/core/auto_indexer.py +3 -3
  65. mcp_vector_search/core/boilerplate.py +186 -0
  66. mcp_vector_search/core/config_utils.py +394 -0
  67. mcp_vector_search/core/database.py +406 -94
  68. mcp_vector_search/core/embeddings.py +24 -0
  69. mcp_vector_search/core/exceptions.py +11 -0
  70. mcp_vector_search/core/git.py +380 -0
  71. mcp_vector_search/core/git_hooks.py +4 -4
  72. mcp_vector_search/core/indexer.py +632 -54
  73. mcp_vector_search/core/llm_client.py +756 -0
  74. mcp_vector_search/core/models.py +91 -1
  75. mcp_vector_search/core/project.py +17 -0
  76. mcp_vector_search/core/relationships.py +473 -0
  77. mcp_vector_search/core/scheduler.py +11 -11
  78. mcp_vector_search/core/search.py +179 -29
  79. mcp_vector_search/mcp/server.py +819 -9
  80. mcp_vector_search/parsers/python.py +285 -5
  81. mcp_vector_search/utils/__init__.py +2 -0
  82. mcp_vector_search/utils/gitignore.py +0 -3
  83. mcp_vector_search/utils/gitignore_updater.py +212 -0
  84. mcp_vector_search/utils/monorepo.py +66 -4
  85. mcp_vector_search/utils/timing.py +10 -6
  86. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
  87. mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
  88. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
  89. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
  90. mcp_vector_search/cli/commands/visualize.py +0 -1467
  91. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  92. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -62,6 +62,11 @@ class EnhancedDidYouMeanTyper(typer.Typer):
62
62
  if click_group is None:
63
63
  return None
64
64
 
65
+ # If click_group is an integer, it's an exit code from standalone_mode=False
66
+ # Return it as-is to preserve exit code propagation
67
+ if isinstance(click_group, int):
68
+ return click_group
69
+
65
70
  # Create enhanced DYM group with original group's properties
66
71
  enhanced_group = EnhancedDidYouMeanGroup(
67
72
  name=click_group.name,
@@ -212,6 +217,17 @@ COMMON_TYPOS = {
212
217
  "grep": "search",
213
218
  "s": "search", # Single letter shortcut
214
219
  "f": "search", # Alternative shortcut for find
220
+ # Chat command variations
221
+ "cht": "chat",
222
+ "caht": "chat",
223
+ "chta": "chat",
224
+ "ask": "chat",
225
+ "question": "chat",
226
+ "qa": "chat",
227
+ "llm": "chat",
228
+ "gpt": "chat",
229
+ "explain": "chat",
230
+ "answer": "chat",
215
231
  # Index command variations
216
232
  "indx": "index",
217
233
  "idx": "index",
@@ -333,9 +349,18 @@ COMMAND_INFO = {
333
349
  "examples": [
334
350
  'mcp-vector-search search "authentication function"',
335
351
  'mcp-vector-search search "error handling" --limit 5',
336
- 'mcp-vector-search find "database connection"',
352
+ 'mcp-vector-search search --files "*.ts" "query"',
353
+ ],
354
+ "related": ["chat", "index", "status"],
355
+ },
356
+ "chat": {
357
+ "description": "Ask AI questions about your code (requires API key)",
358
+ "examples": [
359
+ 'mcp-vector-search chat "where is the database configured?"',
360
+ 'mcp-vector-search chat "how does authentication work?"',
361
+ 'mcp-vector-search chat --limit 3 "explain error handling"',
337
362
  ],
338
- "related": ["search-similar", "search-context", "find", "interactive"],
363
+ "related": ["search", "status", "index"],
339
364
  },
340
365
  "index": {
341
366
  "description": "Index codebase for semantic search",
@@ -1,5 +1,8 @@
1
1
  """Main CLI application for MCP Vector Search."""
2
2
 
3
+ import faulthandler
4
+ import signal
5
+ import sys
3
6
  from pathlib import Path
4
7
 
5
8
  import typer
@@ -9,9 +12,52 @@ from rich.traceback import install
9
12
 
10
13
  from .. import __build__, __version__
11
14
  from .didyoumean import add_common_suggestions, create_enhanced_typer
12
- from .output import print_warning, setup_logging
15
+ from .output import setup_logging
13
16
  from .suggestions import get_contextual_suggestions
14
17
 
18
+
19
+ # ============================================================================
20
+ # SIGNAL HANDLERS - Register early for crash diagnostics
21
+ # ============================================================================
22
+ def _handle_segfault(signum: int, frame) -> None:
23
+ """Handle segmentation faults with helpful error message.
24
+
25
+ Segmentation faults typically occur due to corrupted ChromaDB index data
26
+ or issues with native libraries (sentence-transformers, tree-sitter).
27
+
28
+ Args:
29
+ signum: Signal number (SIGSEGV = 11)
30
+ frame: Current stack frame (unused)
31
+ """
32
+ error_message = """
33
+ ╭─────────────────────────────────────────────────────────────────╮
34
+ │ ⚠️ Segmentation Fault Detected │
35
+ ├─────────────────────────────────────────────────────────────────┤
36
+ │ This usually indicates corrupted index data or a crash in │
37
+ │ native libraries (ChromaDB, sentence-transformers, tree-sitter).│
38
+ │ │
39
+ │ To fix this, please run: │
40
+ │ 1. mcp-vector-search reset index --force │
41
+ │ 2. mcp-vector-search index │
42
+ │ │
43
+ │ This will rebuild your search index from scratch. │
44
+ │ │
45
+ │ If the problem persists: │
46
+ │ - Try updating dependencies: pip install -U mcp-vector-search │
47
+ │ - Check GitHub issues: github.com/bobmatnyc/mcp-vector-search │
48
+ ╰─────────────────────────────────────────────────────────────────╯
49
+ """
50
+ print(error_message, file=sys.stderr)
51
+ sys.exit(139) # Standard segfault exit code (128 + 11)
52
+
53
+
54
+ # Register signal handler for segmentation faults
55
+ signal.signal(signal.SIGSEGV, _handle_segfault)
56
+
57
+ # Enable faulthandler for better crash diagnostics
58
+ # This prints Python traceback on segfaults before signal handler runs
59
+ faulthandler.enable()
60
+
15
61
  # Install rich traceback handler
16
62
  install(show_locals=True)
17
63
 
@@ -22,45 +68,65 @@ console = Console()
22
68
  app = create_enhanced_typer(
23
69
  name="mcp-vector-search",
24
70
  help="""
25
- 🔍 [bold]CLI-first semantic code search with MCP integration[/bold]
26
-
27
- Semantic search finds code by meaning, not just keywords. Perfect for exploring
28
- unfamiliar codebases, finding similar patterns, and integrating with AI tools.
29
-
30
- [bold cyan]Quick Start:[/bold cyan]
31
- 1. Initialize: [green]mcp-vector-search init[/green]
32
- 2. Search code: [green]mcp-vector-search search "your query"[/green]
33
- 3. Check status: [green]mcp-vector-search status[/green]
34
-
35
- [bold cyan]Main Commands:[/bold cyan]
71
+ 🔍 [bold]MCP Vector Search - Semantic Code Search CLI[/bold]
72
+
73
+ Search your codebase by meaning, not just keywords. Find similar code patterns,
74
+ explore unfamiliar projects, and integrate with AI coding tools via MCP.
75
+
76
+ [bold cyan]QUICK START:[/bold cyan]
77
+ mcp-vector-search setup # One-time setup (recommended)
78
+ mcp-vector-search search "query" # Search by meaning
79
+ mcp-vector-search chat "question" # Ask AI about your code
80
+
81
+ [bold cyan]MAIN COMMANDS:[/bold cyan]
82
+ setup 🚀 Zero-config setup (indexes + configures MCP)
83
+ search 🔍 Semantic search (finds code by meaning)
84
+ chat/ask 🤖 LLM-powered Q&A about your code (needs API key)
85
+ status 📊 Show project status
86
+ visualize 📊 Interactive code graph
87
+
88
+ [bold cyan]AI CHAT SETUP:[/bold cyan]
89
+ The 'chat' command requires an OpenRouter API key:
90
+ 1. Get key: [cyan]https://openrouter.ai/keys[/cyan]
91
+ 2. Set: [yellow]export OPENROUTER_API_KEY='your-key'[/yellow]
92
+
93
+ [bold cyan]EXAMPLES:[/bold cyan]
94
+ mcp-vector-search search "error handling"
95
+ mcp-vector-search search --files "*.ts" "authentication"
96
+ mcp-vector-search chat "where is the database configured?"
97
+ mcp-vector-search ask "how does auth work in this project?"
98
+
99
+ [bold cyan]MORE COMMANDS:[/bold cyan]
36
100
  install 📦 Install project and MCP integrations
37
101
  uninstall 🗑️ Remove MCP integrations
38
- init 🔧 Initialize project (simple)
102
+ init 🔧 Initialize project (advanced)
39
103
  demo 🎬 Run interactive demo
40
104
  doctor 🩺 Check system health
41
- status 📊 Show project status
42
- search 🔍 Search code semantically
43
105
  index 📇 Index codebase
106
+ reset 🔄 Reset and recovery operations
44
107
  mcp 🔌 MCP server operations
45
108
  config ⚙️ Configure settings
46
- visualize 📊 Visualize code relationships
47
109
  help ❓ Get help
48
110
  version ℹ️ Show version
49
111
 
50
- [dim]For detailed help: [cyan]mcp-vector-search COMMAND --help[/cyan][/dim]
112
+ [dim]For more: [cyan]mcp-vector-search COMMAND --help[/cyan][/dim]
51
113
  """,
52
114
  add_completion=False,
53
115
  rich_markup_mode="rich",
54
116
  )
55
117
 
56
118
  # Import command modules
119
+ from .commands.analyze import analyze_app # noqa: E402
120
+ from .commands.chat import chat_app # noqa: E402
57
121
  from .commands.config import config_app # noqa: E402
58
122
  from .commands.demo import demo_app # noqa: E402
59
123
  from .commands.index import index_app # noqa: E402
60
124
  from .commands.init import init_app # noqa: E402
61
125
  from .commands.install import install_app # noqa: E402
62
126
  from .commands.mcp import mcp_app # noqa: E402
127
+ from .commands.reset import reset_app # noqa: E402
63
128
  from .commands.search import search_app, search_main # noqa: E402, F401
129
+ from .commands.setup import setup_app # noqa: E402
64
130
  from .commands.status import main as status_main # noqa: E402
65
131
  from .commands.uninstall import uninstall_app # noqa: E402
66
132
  from .commands.visualize import app as visualize_app # noqa: E402
@@ -69,6 +135,9 @@ from .commands.visualize import app as visualize_app # noqa: E402
69
135
  # MAIN COMMANDS - Clean hierarchy
70
136
  # ============================================================================
71
137
 
138
+ # 0. SETUP - Smart zero-config setup (RECOMMENDED!)
139
+ app.add_typer(setup_app, name="setup", help="🚀 Smart zero-config setup (recommended)")
140
+
72
141
  # 1. INSTALL - Install project and MCP integrations (NEW!)
73
142
  app.add_typer(
74
143
  install_app, name="install", help="📦 Install project and MCP integrations"
@@ -95,6 +164,12 @@ app.command("status", help="📊 Show project status and statistics")(status_mai
95
164
  # Register search as both a command and a typer group
96
165
  app.add_typer(search_app, name="search", help="🔍 Search code semantically")
97
166
 
167
+ # 7.5. CHAT - LLM-powered intelligent search
168
+ app.add_typer(chat_app, name="chat", help="🤖 Ask questions about code with LLM")
169
+ app.add_typer(
170
+ chat_app, name="ask", help="🤖 Ask questions about code with LLM (alias for chat)"
171
+ )
172
+
98
173
  # 8. INDEX - Index codebase
99
174
  app.add_typer(index_app, name="index", help="📇 Index codebase for semantic search")
100
175
 
@@ -104,152 +179,26 @@ app.add_typer(mcp_app, name="mcp", help="🔌 MCP server operations")
104
179
  # 10. CONFIG - Configuration
105
180
  app.add_typer(config_app, name="config", help="⚙️ Manage project configuration")
106
181
 
107
- # 11. VISUALIZE - Code graph visualization
182
+ # 10.5. RESET - Reset and recovery operations
183
+ app.add_typer(reset_app, name="reset", help="🔄 Reset and recovery operations")
184
+
185
+ # 11. ANALYZE - Code complexity analysis
186
+ app.add_typer(
187
+ analyze_app, name="analyze", help="📈 Analyze code complexity and quality"
188
+ )
189
+
190
+ # 12. VISUALIZE - Code graph visualization
108
191
  app.add_typer(
109
192
  visualize_app, name="visualize", help="📊 Visualize code chunk relationships"
110
193
  )
111
194
 
112
- # 12. HELP - Enhanced help
195
+ # 13. HELP - Enhanced help
113
196
  # (defined below inline)
114
197
 
115
- # 13. VERSION - Version info
198
+ # 14. VERSION - Version info
116
199
  # (defined below inline)
117
200
 
118
201
 
119
- # ============================================================================
120
- # DEPRECATED COMMANDS - With helpful suggestions
121
- # ============================================================================
122
-
123
-
124
- def _deprecated_command(old_cmd: str, new_cmd: str):
125
- """Helper to create deprecated command with suggestion."""
126
-
127
- def wrapper(*args, **kwargs):
128
- print_warning(
129
- f"⚠️ The command '{old_cmd}' is deprecated.\n"
130
- f" Please use '{new_cmd}' instead.\n"
131
- f" Run: [cyan]mcp-vector-search {new_cmd} --help[/cyan] for details."
132
- )
133
- raise typer.Exit(1)
134
-
135
- return wrapper
136
-
137
-
138
- # NOTE: 'install' command is now the primary command for project installation
139
- # Old 'install' was deprecated in favor of 'init' in v0.7.0
140
- # Now 'install' is back as the hierarchical installation command in v0.13.0
141
-
142
-
143
- # Deprecated: find -> search
144
- @app.command("find", hidden=True)
145
- def deprecated_find():
146
- """[DEPRECATED] Use 'search' instead."""
147
- _deprecated_command("find", "search")()
148
-
149
-
150
- # Deprecated: search-similar -> search --similar
151
- @app.command("search-similar", hidden=True)
152
- def deprecated_search_similar():
153
- """[DEPRECATED] Use 'search --similar' instead."""
154
- _deprecated_command("search-similar", "search --similar")()
155
-
156
-
157
- # Deprecated: search-context -> search --context
158
- @app.command("search-context", hidden=True)
159
- def deprecated_search_context():
160
- """[DEPRECATED] Use 'search --context' instead."""
161
- _deprecated_command("search-context", "search --context")()
162
-
163
-
164
- # Deprecated: interactive -> search interactive
165
- @app.command("interactive", hidden=True)
166
- def deprecated_interactive():
167
- """[DEPRECATED] Use 'search interactive' instead."""
168
- _deprecated_command("interactive", "search interactive")()
169
-
170
-
171
- # Deprecated: history -> search history
172
- @app.command("history", hidden=True)
173
- def deprecated_history():
174
- """[DEPRECATED] Use 'search history' instead."""
175
- _deprecated_command("history", "search history")()
176
-
177
-
178
- # Deprecated: favorites -> search favorites
179
- @app.command("favorites", hidden=True)
180
- def deprecated_favorites():
181
- """[DEPRECATED] Use 'search favorites' instead."""
182
- _deprecated_command("favorites", "search favorites")()
183
-
184
-
185
- # Deprecated: add-favorite -> search favorites add
186
- @app.command("add-favorite", hidden=True)
187
- def deprecated_add_favorite():
188
- """[DEPRECATED] Use 'search favorites add' instead."""
189
- _deprecated_command("add-favorite", "search favorites add")()
190
-
191
-
192
- # Deprecated: remove-favorite -> search favorites remove
193
- @app.command("remove-favorite", hidden=True)
194
- def deprecated_remove_favorite():
195
- """[DEPRECATED] Use 'search favorites remove' instead."""
196
- _deprecated_command("remove-favorite", "search favorites remove")()
197
-
198
-
199
- # Deprecated: health -> index health
200
- @app.command("health", hidden=True)
201
- def deprecated_health():
202
- """[DEPRECATED] Use 'index health' instead."""
203
- _deprecated_command("health", "index health")()
204
-
205
-
206
- # Deprecated: watch -> index watch
207
- @app.command("watch", hidden=True)
208
- def deprecated_watch():
209
- """[DEPRECATED] Use 'index watch' instead."""
210
- _deprecated_command("watch", "index watch")()
211
-
212
-
213
- # Deprecated: auto-index -> index auto
214
- @app.command("auto-index", hidden=True)
215
- def deprecated_auto_index():
216
- """[DEPRECATED] Use 'index auto' instead."""
217
- _deprecated_command("auto-index", "index auto")()
218
-
219
-
220
- # Deprecated: reset -> mcp reset or config reset
221
- @app.command("reset", hidden=True)
222
- def deprecated_reset():
223
- """[DEPRECATED] Use 'mcp reset' or 'config reset' instead."""
224
- print_warning(
225
- "⚠️ The 'reset' command is deprecated.\n"
226
- " Use [cyan]mcp-vector-search mcp reset[/cyan] for MCP reset\n"
227
- " Use [cyan]mcp-vector-search config reset[/cyan] for config reset"
228
- )
229
- raise typer.Exit(1)
230
-
231
-
232
- # Deprecated: init-check -> init check
233
- @app.command("init-check", hidden=True)
234
- def deprecated_init_check():
235
- """[DEPRECATED] Use 'init check' instead."""
236
- _deprecated_command("init-check", "init check")()
237
-
238
-
239
- # Deprecated: init-mcp -> mcp install
240
- @app.command("init-mcp", hidden=True)
241
- def deprecated_init_mcp():
242
- """[DEPRECATED] Use 'mcp install' instead."""
243
- _deprecated_command("init-mcp", "mcp install")()
244
-
245
-
246
- # Deprecated: init-models -> config models
247
- @app.command("init-models", hidden=True)
248
- def deprecated_init_models():
249
- """[DEPRECATED] Use 'config models' instead."""
250
- _deprecated_command("init-models", "config models")()
251
-
252
-
253
202
  # ============================================================================
254
203
  # MAIN INLINE COMMANDS
255
204
  # ============================================================================
@@ -329,6 +278,15 @@ def version_command() -> None:
329
278
  console.print("[dim]Built with ChromaDB, Tree-sitter, and modern Python[/dim]")
330
279
 
331
280
 
281
+ def _version_callback(value: bool) -> None:
282
+ """Handle --version flag eagerly before command parsing."""
283
+ if value:
284
+ console.print(
285
+ f"[bold blue]mcp-vector-search[/bold blue] version [green]{__version__}[/green] [dim](build {__build__})[/dim]"
286
+ )
287
+ raise typer.Exit()
288
+
289
+
332
290
  @app.callback()
333
291
  def main(
334
292
  ctx: typer.Context,
@@ -338,6 +296,8 @@ def main(
338
296
  "-v",
339
297
  help="Show version and exit",
340
298
  rich_help_panel="ℹ️ Information",
299
+ is_eager=True,
300
+ callback=lambda v: _version_callback(v),
341
301
  ),
342
302
  verbose: bool = typer.Option(
343
303
  False,
@@ -368,9 +328,8 @@ def main(
368
328
  A modern, lightweight tool for semantic code search using ChromaDB and Tree-sitter.
369
329
  Designed for local development with optional MCP server integration.
370
330
  """
371
- if version:
372
- console.print(f"mcp-vector-search version {__version__} (build {__build__})")
373
- raise typer.Exit()
331
+ # Note: --version is handled by _version_callback with is_eager=True
332
+ # This ensures it runs before no_args_is_help check
374
333
 
375
334
  # Setup logging
376
335
  log_level = "DEBUG" if verbose else "ERROR" if quiet else "WARNING"
@@ -401,7 +360,11 @@ def cli_with_suggestions():
401
360
 
402
361
  try:
403
362
  # Call the app with standalone_mode=False to get exceptions instead of sys.exit
404
- app(standalone_mode=False)
363
+ # Capture return value - when standalone_mode=False, typer.Exit returns code instead of raising
364
+ exit_code = app(standalone_mode=False)
365
+ # Propagate non-zero exit codes (e.g., from --fail-on-smell quality gate)
366
+ if exit_code is not None and exit_code != 0:
367
+ sys.exit(exit_code)
405
368
  except click.UsageError as e:
406
369
  # Check if it's a "No such command" error
407
370
  if "No such command" in str(e):
@@ -439,8 +402,12 @@ def cli_with_suggestions():
439
402
  except click.Abort:
440
403
  # User interrupted (Ctrl+C)
441
404
  sys.exit(1)
442
- except SystemExit:
443
- # Re-raise system exits
405
+ except (SystemExit, click.exceptions.Exit) as e:
406
+ # Re-raise system exits and typer.Exit with their exit codes
407
+ if hasattr(e, "exit_code"):
408
+ sys.exit(e.exit_code)
409
+ elif hasattr(e, "code"):
410
+ sys.exit(e.code if e.code is not None else 0)
444
411
  raise
445
412
  except Exception as e:
446
413
  # For other exceptions, show error and exit if verbose logging is enabled
@@ -24,6 +24,46 @@ from ..core.models import ProjectInfo, SearchResult
24
24
  console = Console()
25
25
 
26
26
 
27
+ def _get_grade_color(grade: str) -> str:
28
+ """Get color for complexity grade."""
29
+ grade_colors = {
30
+ "A": "green",
31
+ "B": "cyan",
32
+ "C": "yellow",
33
+ "D": "orange",
34
+ "F": "red",
35
+ }
36
+ return grade_colors.get(grade, "white")
37
+
38
+
39
+ def _get_complexity_color(complexity: int) -> str:
40
+ """Get color based on cognitive complexity value."""
41
+ if complexity <= 5:
42
+ return "green"
43
+ elif complexity <= 10:
44
+ return "cyan"
45
+ elif complexity <= 20:
46
+ return "yellow"
47
+ elif complexity <= 30:
48
+ return "orange"
49
+ else:
50
+ return "red"
51
+
52
+
53
+ def _get_quality_color(quality: int) -> str:
54
+ """Get color based on quality score (0-100)."""
55
+ if quality >= 80:
56
+ return "green"
57
+ elif quality >= 60:
58
+ return "cyan"
59
+ elif quality >= 40:
60
+ return "yellow"
61
+ elif quality >= 20:
62
+ return "orange"
63
+ else:
64
+ return "red"
65
+
66
+
27
67
  def setup_logging(level: str = "WARNING") -> None:
28
68
  """Setup structured logging with rich formatting.
29
69
 
@@ -97,9 +137,11 @@ def print_project_info(project_info: ProjectInfo) -> None:
97
137
  table.add_row("Initialized", "✓" if project_info.is_initialized else "✗")
98
138
  table.add_row(
99
139
  "Languages",
100
- ", ".join(project_info.languages)
101
- if project_info.languages
102
- else "None detected",
140
+ (
141
+ ", ".join(project_info.languages)
142
+ if project_info.languages
143
+ else "None detected"
144
+ ),
103
145
  )
104
146
  table.add_row("Indexable Files", str(project_info.file_count))
105
147
 
@@ -111,8 +153,17 @@ def print_search_results(
111
153
  query: str,
112
154
  show_content: bool = True,
113
155
  max_content_lines: int = 10,
156
+ quality_weight: float = 0.0,
114
157
  ) -> None:
115
- """Print search results in a formatted display."""
158
+ """Print search results in a formatted display with quality-aware ranking.
159
+
160
+ Args:
161
+ results: List of search results
162
+ query: Original search query
163
+ show_content: Whether to show code content
164
+ max_content_lines: Maximum lines of code to show
165
+ quality_weight: Weight for quality ranking (0.0-1.0), used to show score breakdown
166
+ """
116
167
  if not results:
117
168
  print_warning(f"No results found for query: '{query}'")
118
169
  return
@@ -120,7 +171,14 @@ def print_search_results(
120
171
  console.print(
121
172
  f"\n[bold blue]Search Results for:[/bold blue] [green]'{query}'[/green]"
122
173
  )
123
- console.print(f"[dim]Found {len(results)} results[/dim]\n")
174
+
175
+ # Show quality ranking info if enabled
176
+ if quality_weight > 0.0:
177
+ console.print(
178
+ f"[dim]Found {len(results)} results (quality-aware ranking: {quality_weight:.0%} quality, {(1 - quality_weight):.0%} relevance)[/dim]\n"
179
+ )
180
+ else:
181
+ console.print(f"[dim]Found {len(results)} results[/dim]\n")
124
182
 
125
183
  for i, result in enumerate(results, 1):
126
184
  # Create result header
@@ -130,12 +188,85 @@ def print_search_results(
130
188
  if result.class_name:
131
189
  header += f" in [yellow]{result.class_name}[/yellow]"
132
190
 
133
- # Add location and similarity
191
+ # Add location
134
192
  location = f"[dim]{result.location}[/dim]"
135
- similarity = f"[green]{result.similarity_score:.2%}[/green]"
136
193
 
137
194
  console.print(f"{header}")
138
- console.print(f" {location} | Similarity: {similarity}")
195
+
196
+ # Build metadata line with quality metrics
197
+ metadata_parts = [location]
198
+
199
+ # Show score breakdown if quality ranking is enabled
200
+ if quality_weight > 0.0 and hasattr(result, "_original_similarity"):
201
+ # Quality-aware ranking: show relevance, quality, and combined
202
+ relevance_score = result._original_similarity
203
+ combined_score = result.similarity_score
204
+ quality_score = result.quality_score or 0
205
+
206
+ metadata_parts.append(f"Relevance: [cyan]{relevance_score:.2%}[/cyan]")
207
+ metadata_parts.append(
208
+ f"Quality: [{_get_quality_color(quality_score)}]{quality_score}[/{_get_quality_color(quality_score)}]"
209
+ )
210
+ metadata_parts.append(f"Combined: [green]{combined_score:.2%}[/green]")
211
+ else:
212
+ # Pure semantic search: show only similarity score
213
+ similarity = f"[green]{result.similarity_score:.2%}[/green]"
214
+ metadata_parts.append(f"Similarity: {similarity}")
215
+
216
+ console.print(f" {' | '.join(metadata_parts)}")
217
+
218
+ # Add quality indicator line if quality metrics are available and not shown in scores
219
+ if result.complexity_grade and quality_weight == 0.0:
220
+ # Show quality metrics when not using quality ranking
221
+ quality_indicators = []
222
+
223
+ grade_color = _get_grade_color(result.complexity_grade)
224
+ quality_indicators.append(
225
+ f"Grade: [{grade_color}]{result.complexity_grade}[/{grade_color}]"
226
+ )
227
+
228
+ if result.cognitive_complexity is not None:
229
+ complexity_color = _get_complexity_color(result.cognitive_complexity)
230
+ quality_indicators.append(
231
+ f"Complexity: [{complexity_color}]{result.cognitive_complexity}[/{complexity_color}]"
232
+ )
233
+
234
+ # Show quality indicator with check/cross
235
+ smell_count = result.smell_count or 0
236
+ if smell_count == 0:
237
+ console.print(
238
+ f" [green]✓[/green] {' | '.join(quality_indicators)} | No smells"
239
+ )
240
+ else:
241
+ # List smells if available
242
+ smells_text = f"{smell_count} smells"
243
+ if result.code_smells:
244
+ smell_names = ", ".join(result.code_smells[:3]) # Show first 3
245
+ if len(result.code_smells) > 3:
246
+ smell_names += f", +{len(result.code_smells) - 3} more"
247
+ smells_text = f"{smell_count} smells: [dim]{smell_names}[/dim]"
248
+
249
+ console.print(
250
+ f" [red]✗[/red] {' | '.join(quality_indicators)} | {smells_text}"
251
+ )
252
+ elif result.complexity_grade and quality_weight > 0.0:
253
+ # When using quality ranking, show simpler quality indicator
254
+ smell_count = result.smell_count or 0
255
+ if smell_count == 0:
256
+ console.print(
257
+ f" [green]✓[/green] Grade {result.complexity_grade}, No smells"
258
+ )
259
+ else:
260
+ smells_text = (
261
+ ", ".join(result.code_smells[:3])
262
+ if result.code_smells
263
+ else f"{smell_count} smells"
264
+ )
265
+ if result.code_smells and len(result.code_smells) > 3:
266
+ smells_text += f", +{len(result.code_smells) - 3} more"
267
+ console.print(
268
+ f" [red]✗[/red] Grade {result.complexity_grade}, {smells_text}"
269
+ )
139
270
 
140
271
  # Show code content if requested
141
272
  if show_content and result.content:
@@ -248,16 +379,30 @@ def print_dependency_status(
248
379
 
249
380
 
250
381
  def print_json(data: Any, title: str | None = None) -> None:
251
- """Print data as formatted JSON."""
252
- import json
382
+ """Print data as formatted JSON.
383
+
384
+ When title is None (typical for --json flag), outputs raw JSON to stdout
385
+ for machine consumption (e.g., piping to json.tool).
386
+ When title is provided, uses Rich formatting for human-readable display.
253
387
 
254
- json_str = json.dumps(data, indent=2, default=str)
255
- syntax = Syntax(json_str, "json", theme="monokai")
388
+ Args:
389
+ data: Data to serialize as JSON
390
+ title: Optional title for Rich panel display
391
+ """
392
+ import json
256
393
 
257
394
  if title:
395
+ # Human-readable display with Rich formatting
396
+ json_str = json.dumps(data, indent=2, default=str)
397
+ syntax = Syntax(json_str, "json", theme="monokai")
258
398
  console.print(Panel(syntax, title=title, border_style="blue"))
259
399
  else:
260
- console.print(syntax)
400
+ # Machine-readable output: raw JSON to stdout
401
+ # Use sys.stdout.write to bypass Rich console formatting
402
+ json_str = json.dumps(data, indent=2, default=str, ensure_ascii=False)
403
+ sys.stdout.write(json_str)
404
+ sys.stdout.write("\n")
405
+ sys.stdout.flush()
261
406
 
262
407
 
263
408
  def print_panel(
@@ -1 +1,5 @@
1
1
  """Configuration management for MCP Vector Search."""
2
+
3
+ from .thresholds import ComplexityThresholds, SmellThresholds, ThresholdConfig
4
+
5
+ __all__ = ["ComplexityThresholds", "SmellThresholds", "ThresholdConfig"]