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
@@ -0,0 +1,394 @@
1
+ """Secure configuration utilities for API key storage and retrieval.
2
+
3
+ This module provides utilities for securely storing and retrieving sensitive
4
+ configuration data like API keys. Keys are stored in the project's config
5
+ directory with restrictive file permissions.
6
+
7
+ Design Decision: Local file storage with permissions
8
+ - Rationale: Simple, works cross-platform, no external dependencies
9
+ - Trade-offs: Not encrypted at rest (OS file permissions only), single-machine
10
+ - Alternatives considered:
11
+ 1. OS keyring (rejected: platform-specific, complex setup)
12
+ 2. Encrypted storage (rejected: key management complexity)
13
+ 3. Environment variables only (rejected: poor user experience)
14
+
15
+ Priority: Environment variable (OPENROUTER_API_KEY) > Local config file
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import stat
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from loguru import logger
25
+
26
+ # Configuration file name
27
+ CONFIG_FILENAME = "config.json"
28
+
29
+ # Sensitive keys that should never be logged in full
30
+ SENSITIVE_KEYS = {"openrouter_api_key", "openai_api_key", "api_key", "token", "secret"}
31
+
32
+
33
+ class ConfigManager:
34
+ """Secure configuration manager for API keys and sensitive data.
35
+
36
+ Handles reading/writing configuration with proper error handling
37
+ and security considerations.
38
+
39
+ Security Features:
40
+ - Restrictive file permissions (0600 - owner read/write only)
41
+ - Sensitive values masked in logs
42
+ - Environment variable override support
43
+ """
44
+
45
+ def __init__(self, config_dir: Path) -> None:
46
+ """Initialize config manager.
47
+
48
+ Args:
49
+ config_dir: Directory to store config file (e.g., .mcp-vector-search/)
50
+ """
51
+ self.config_dir = config_dir
52
+ self.config_file = config_dir / CONFIG_FILENAME
53
+
54
+ def _ensure_config_dir(self) -> None:
55
+ """Ensure config directory exists with proper permissions."""
56
+ if not self.config_dir.exists():
57
+ self.config_dir.mkdir(parents=True, exist_ok=True)
58
+ # Set directory permissions to owner-only (0700)
59
+ self.config_dir.chmod(stat.S_IRWXU)
60
+ logger.debug(f"Created config directory: {self.config_dir}")
61
+
62
+ def _set_secure_permissions(self, file_path: Path) -> None:
63
+ """Set restrictive file permissions (owner read/write only).
64
+
65
+ Args:
66
+ file_path: Path to file to secure
67
+
68
+ Note: Sets 0600 permissions (rw-------) on Unix-like systems.
69
+ Windows uses different permission model but respects intent.
70
+ """
71
+ try:
72
+ # Set to 0600 (rw-------)
73
+ file_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
74
+ logger.debug(f"Set secure permissions (0600) on {file_path}")
75
+ except Exception as e:
76
+ logger.warning(f"Failed to set secure permissions on {file_path}: {e}")
77
+
78
+ def _mask_sensitive_value(self, value: str) -> str:
79
+ """Mask sensitive value for logging (show last 4 chars only).
80
+
81
+ Args:
82
+ value: Value to mask
83
+
84
+ Returns:
85
+ Masked value like "****1234"
86
+ """
87
+ if not value or len(value) < 4:
88
+ return "****"
89
+ return "*" * (len(value) - 4) + value[-4:]
90
+
91
+ def load_config(self) -> dict[str, Any]:
92
+ """Load configuration from file.
93
+
94
+ Returns:
95
+ Configuration dictionary (empty dict if file doesn't exist)
96
+
97
+ Error Handling:
98
+ - Missing file: Returns empty dict
99
+ - JSON parse error: Logs warning, returns empty dict
100
+ - Read error: Logs error, returns empty dict
101
+ """
102
+ if not self.config_file.exists():
103
+ logger.debug(f"Config file not found: {self.config_file}")
104
+ return {}
105
+
106
+ try:
107
+ with open(self.config_file) as f:
108
+ config = json.load(f)
109
+ logger.debug(f"Loaded config from {self.config_file}")
110
+ return config
111
+ except json.JSONDecodeError as e:
112
+ logger.warning(f"Invalid JSON in config file {self.config_file}: {e}")
113
+ return {}
114
+ except Exception as e:
115
+ logger.error(f"Failed to load config from {self.config_file}: {e}")
116
+ return {}
117
+
118
+ def save_config(self, config: dict[str, Any]) -> None:
119
+ """Save configuration to file with secure permissions.
120
+
121
+ Args:
122
+ config: Configuration dictionary to save
123
+
124
+ Raises:
125
+ OSError: If file write fails
126
+ ValueError: If config is not JSON-serializable
127
+ """
128
+ self._ensure_config_dir()
129
+
130
+ try:
131
+ # Write to temporary file first (atomic write)
132
+ temp_file = self.config_file.with_suffix(".tmp")
133
+
134
+ with open(temp_file, "w") as f:
135
+ json.dump(config, f, indent=2)
136
+
137
+ # Set secure permissions before moving
138
+ self._set_secure_permissions(temp_file)
139
+
140
+ # Atomic move
141
+ temp_file.replace(self.config_file)
142
+
143
+ logger.debug(f"Saved config to {self.config_file}")
144
+
145
+ except Exception as e:
146
+ logger.error(f"Failed to save config to {self.config_file}: {e}")
147
+ raise
148
+
149
+ def get_value(self, key: str, default: Any = None) -> Any:
150
+ """Get configuration value.
151
+
152
+ Args:
153
+ key: Configuration key
154
+ default: Default value if key not found
155
+
156
+ Returns:
157
+ Configuration value or default
158
+ """
159
+ config = self.load_config()
160
+ return config.get(key, default)
161
+
162
+ def set_value(self, key: str, value: Any) -> None:
163
+ """Set configuration value.
164
+
165
+ Args:
166
+ key: Configuration key
167
+ value: Value to set
168
+ """
169
+ config = self.load_config()
170
+ config[key] = value
171
+ self.save_config(config)
172
+
173
+ # Log with masking for sensitive keys
174
+ if key.lower() in SENSITIVE_KEYS:
175
+ masked = self._mask_sensitive_value(str(value))
176
+ logger.info(f"Set {key} = {masked}")
177
+ else:
178
+ logger.info(f"Set {key} = {value}")
179
+
180
+ def delete_value(self, key: str) -> bool:
181
+ """Delete configuration value.
182
+
183
+ Args:
184
+ key: Configuration key to delete
185
+
186
+ Returns:
187
+ True if key was present and deleted, False otherwise
188
+ """
189
+ config = self.load_config()
190
+ if key in config:
191
+ del config[key]
192
+ self.save_config(config)
193
+ logger.info(f"Deleted {key} from config")
194
+ return True
195
+ return False
196
+
197
+
198
+ def get_openrouter_api_key(config_dir: Path | None = None) -> str | None:
199
+ """Get OpenRouter API key from environment or config file.
200
+
201
+ Priority order:
202
+ 1. OPENROUTER_API_KEY environment variable
203
+ 2. openrouter_api_key in config file
204
+
205
+ Args:
206
+ config_dir: Config directory path (uses .mcp-vector-search in cwd if None)
207
+
208
+ Returns:
209
+ API key if found, None otherwise
210
+ """
211
+ # Check environment variable first (highest priority)
212
+ api_key = os.environ.get("OPENROUTER_API_KEY")
213
+ if api_key:
214
+ logger.debug("Using OpenRouter API key from environment variable")
215
+ return api_key
216
+
217
+ # Check config file
218
+ if config_dir is None:
219
+ config_dir = Path.cwd() / ".mcp-vector-search"
220
+
221
+ if not config_dir.exists():
222
+ logger.debug(f"Config directory not found: {config_dir}")
223
+ return None
224
+
225
+ manager = ConfigManager(config_dir)
226
+ api_key = manager.get_value("openrouter_api_key")
227
+
228
+ if api_key:
229
+ logger.debug("Using OpenRouter API key from config file")
230
+ return api_key
231
+
232
+ logger.debug("OpenRouter API key not found in environment or config")
233
+ return None
234
+
235
+
236
+ def save_openrouter_api_key(api_key: str, config_dir: Path) -> None:
237
+ """Save OpenRouter API key to config file.
238
+
239
+ Args:
240
+ api_key: API key to save
241
+ config_dir: Config directory path
242
+
243
+ Raises:
244
+ ValueError: If api_key is empty
245
+ OSError: If file write fails
246
+ """
247
+ if not api_key or not api_key.strip():
248
+ raise ValueError("API key cannot be empty")
249
+
250
+ manager = ConfigManager(config_dir)
251
+ manager.set_value("openrouter_api_key", api_key.strip())
252
+
253
+ logger.info(
254
+ f"Saved OpenRouter API key to {manager.config_file} "
255
+ f"(last 4 chars: {api_key[-4:]})"
256
+ )
257
+
258
+
259
+ def delete_openrouter_api_key(config_dir: Path) -> bool:
260
+ """Delete OpenRouter API key from config file.
261
+
262
+ Args:
263
+ config_dir: Config directory path
264
+
265
+ Returns:
266
+ True if key was deleted, False if not found
267
+ """
268
+ manager = ConfigManager(config_dir)
269
+ return manager.delete_value("openrouter_api_key")
270
+
271
+
272
+ def get_openai_api_key(config_dir: Path | None = None) -> str | None:
273
+ """Get OpenAI API key from environment or config file.
274
+
275
+ Priority order:
276
+ 1. OPENAI_API_KEY environment variable
277
+ 2. openai_api_key in config file
278
+
279
+ Args:
280
+ config_dir: Config directory path (uses .mcp-vector-search in cwd if None)
281
+
282
+ Returns:
283
+ API key if found, None otherwise
284
+ """
285
+ # Check environment variable first (highest priority)
286
+ api_key = os.environ.get("OPENAI_API_KEY")
287
+ if api_key:
288
+ logger.debug("Using OpenAI API key from environment variable")
289
+ return api_key
290
+
291
+ # Check config file
292
+ if config_dir is None:
293
+ config_dir = Path.cwd() / ".mcp-vector-search"
294
+
295
+ if not config_dir.exists():
296
+ logger.debug(f"Config directory not found: {config_dir}")
297
+ return None
298
+
299
+ manager = ConfigManager(config_dir)
300
+ api_key = manager.get_value("openai_api_key")
301
+
302
+ if api_key:
303
+ logger.debug("Using OpenAI API key from config file")
304
+ return api_key
305
+
306
+ logger.debug("OpenAI API key not found in environment or config")
307
+ return None
308
+
309
+
310
+ def save_openai_api_key(api_key: str, config_dir: Path) -> None:
311
+ """Save OpenAI API key to config file.
312
+
313
+ Args:
314
+ api_key: API key to save
315
+ config_dir: Config directory path
316
+
317
+ Raises:
318
+ ValueError: If api_key is empty
319
+ OSError: If file write fails
320
+ """
321
+ if not api_key or not api_key.strip():
322
+ raise ValueError("API key cannot be empty")
323
+
324
+ manager = ConfigManager(config_dir)
325
+ manager.set_value("openai_api_key", api_key.strip())
326
+
327
+ logger.info(
328
+ f"Saved OpenAI API key to {manager.config_file} (last 4 chars: {api_key[-4:]})"
329
+ )
330
+
331
+
332
+ def delete_openai_api_key(config_dir: Path) -> bool:
333
+ """Delete OpenAI API key from config file.
334
+
335
+ Args:
336
+ config_dir: Config directory path
337
+
338
+ Returns:
339
+ True if key was deleted, False if not found
340
+ """
341
+ manager = ConfigManager(config_dir)
342
+ return manager.delete_value("openai_api_key")
343
+
344
+
345
+ def get_preferred_llm_provider(config_dir: Path | None = None) -> str | None:
346
+ """Get preferred LLM provider from config file.
347
+
348
+ Args:
349
+ config_dir: Config directory path (uses .mcp-vector-search in cwd if None)
350
+
351
+ Returns:
352
+ Provider name ('openai' or 'openrouter') if set, None otherwise
353
+ """
354
+ if config_dir is None:
355
+ config_dir = Path.cwd() / ".mcp-vector-search"
356
+
357
+ if not config_dir.exists():
358
+ return None
359
+
360
+ manager = ConfigManager(config_dir)
361
+ return manager.get_value("preferred_llm_provider")
362
+
363
+
364
+ def save_preferred_llm_provider(provider: str, config_dir: Path) -> None:
365
+ """Save preferred LLM provider to config file.
366
+
367
+ Args:
368
+ provider: Provider name ('openai' or 'openrouter')
369
+ config_dir: Config directory path
370
+
371
+ Raises:
372
+ ValueError: If provider is not valid
373
+ """
374
+ if provider not in ("openai", "openrouter"):
375
+ raise ValueError(
376
+ f"Invalid provider: {provider}. Must be 'openai' or 'openrouter'"
377
+ )
378
+
379
+ manager = ConfigManager(config_dir)
380
+ manager.set_value("preferred_llm_provider", provider)
381
+
382
+
383
+ def get_config_file_path(config_dir: Path | None = None) -> Path:
384
+ """Get path to config file.
385
+
386
+ Args:
387
+ config_dir: Config directory path (uses .mcp-vector-search in cwd if None)
388
+
389
+ Returns:
390
+ Path to config file
391
+ """
392
+ if config_dir is None:
393
+ config_dir = Path.cwd() / ".mcp-vector-search"
394
+ return config_dir / CONFIG_FILENAME