mcp-vector-search 0.0.3__py3-none-any.whl → 0.4.11__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.

Potentially problematic release.


This version of mcp-vector-search might be problematic. Click here for more details.

Files changed (49) hide show
  1. mcp_vector_search/__init__.py +3 -2
  2. mcp_vector_search/cli/commands/auto_index.py +397 -0
  3. mcp_vector_search/cli/commands/config.py +88 -40
  4. mcp_vector_search/cli/commands/index.py +198 -52
  5. mcp_vector_search/cli/commands/init.py +472 -58
  6. mcp_vector_search/cli/commands/install.py +284 -0
  7. mcp_vector_search/cli/commands/mcp.py +495 -0
  8. mcp_vector_search/cli/commands/search.py +241 -87
  9. mcp_vector_search/cli/commands/status.py +184 -58
  10. mcp_vector_search/cli/commands/watch.py +34 -35
  11. mcp_vector_search/cli/didyoumean.py +184 -0
  12. mcp_vector_search/cli/export.py +320 -0
  13. mcp_vector_search/cli/history.py +292 -0
  14. mcp_vector_search/cli/interactive.py +342 -0
  15. mcp_vector_search/cli/main.py +163 -26
  16. mcp_vector_search/cli/output.py +63 -45
  17. mcp_vector_search/config/defaults.py +50 -36
  18. mcp_vector_search/config/settings.py +49 -35
  19. mcp_vector_search/core/auto_indexer.py +298 -0
  20. mcp_vector_search/core/connection_pool.py +322 -0
  21. mcp_vector_search/core/database.py +335 -25
  22. mcp_vector_search/core/embeddings.py +73 -29
  23. mcp_vector_search/core/exceptions.py +19 -2
  24. mcp_vector_search/core/factory.py +310 -0
  25. mcp_vector_search/core/git_hooks.py +345 -0
  26. mcp_vector_search/core/indexer.py +237 -73
  27. mcp_vector_search/core/models.py +21 -19
  28. mcp_vector_search/core/project.py +73 -58
  29. mcp_vector_search/core/scheduler.py +330 -0
  30. mcp_vector_search/core/search.py +574 -86
  31. mcp_vector_search/core/watcher.py +48 -46
  32. mcp_vector_search/mcp/__init__.py +4 -0
  33. mcp_vector_search/mcp/__main__.py +25 -0
  34. mcp_vector_search/mcp/server.py +701 -0
  35. mcp_vector_search/parsers/base.py +30 -31
  36. mcp_vector_search/parsers/javascript.py +74 -48
  37. mcp_vector_search/parsers/python.py +57 -49
  38. mcp_vector_search/parsers/registry.py +47 -32
  39. mcp_vector_search/parsers/text.py +179 -0
  40. mcp_vector_search/utils/__init__.py +40 -0
  41. mcp_vector_search/utils/gitignore.py +229 -0
  42. mcp_vector_search/utils/timing.py +334 -0
  43. mcp_vector_search/utils/version.py +47 -0
  44. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/METADATA +173 -7
  45. mcp_vector_search-0.4.11.dist-info/RECORD +54 -0
  46. mcp_vector_search-0.0.3.dist-info/RECORD +0 -35
  47. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/WHEEL +0 -0
  48. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/entry_points.txt +0 -0
  49. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,6 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
- from typing import List, Optional, Set
6
5
 
7
6
  from loguru import logger
8
7
 
@@ -14,6 +13,7 @@ from ..config.defaults import (
14
13
  get_language_from_extension,
15
14
  )
16
15
  from ..config.settings import ProjectConfig
16
+ from ..utils.gitignore import create_gitignore_parser
17
17
  from .exceptions import (
18
18
  ConfigurationError,
19
19
  ProjectInitializationError,
@@ -25,19 +25,26 @@ from .models import ProjectInfo
25
25
  class ProjectManager:
26
26
  """Manages project detection, initialization, and configuration."""
27
27
 
28
- def __init__(self, project_root: Optional[Path] = None) -> None:
28
+ def __init__(self, project_root: Path | None = None) -> None:
29
29
  """Initialize project manager.
30
-
30
+
31
31
  Args:
32
32
  project_root: Project root directory. If None, will auto-detect.
33
33
  """
34
34
  self.project_root = project_root or self._detect_project_root()
35
- self._config: Optional[ProjectConfig] = None
35
+ self._config: ProjectConfig | None = None
36
+
37
+ # Initialize gitignore parser
38
+ try:
39
+ self.gitignore_parser = create_gitignore_parser(self.project_root)
40
+ except Exception as e:
41
+ logger.debug(f"Failed to load gitignore patterns: {e}")
42
+ self.gitignore_parser = None
36
43
 
37
44
  def _detect_project_root(self) -> Path:
38
45
  """Auto-detect project root directory."""
39
46
  current = Path.cwd()
40
-
47
+
41
48
  # Look for common project indicators
42
49
  indicators = [
43
50
  ".git",
@@ -50,14 +57,14 @@ class ProjectManager:
50
57
  "build.gradle",
51
58
  ".project",
52
59
  ]
53
-
60
+
54
61
  # Walk up the directory tree
55
62
  for path in [current] + list(current.parents):
56
63
  for indicator in indicators:
57
64
  if (path / indicator).exists():
58
65
  logger.debug(f"Detected project root: {path} (found {indicator})")
59
66
  return path
60
-
67
+
61
68
  # Default to current directory
62
69
  logger.debug(f"Using current directory as project root: {current}")
63
70
  return current
@@ -66,27 +73,27 @@ class ProjectManager:
66
73
  """Check if project is initialized for MCP Vector Search."""
67
74
  config_path = get_default_config_path(self.project_root)
68
75
  index_path = get_default_index_path(self.project_root)
69
-
76
+
70
77
  return config_path.exists() and index_path.exists()
71
78
 
72
79
  def initialize(
73
80
  self,
74
- file_extensions: Optional[List[str]] = None,
81
+ file_extensions: list[str] | None = None,
75
82
  embedding_model: str = "microsoft/codebert-base",
76
- similarity_threshold: float = 0.75,
83
+ similarity_threshold: float = 0.5,
77
84
  force: bool = False,
78
85
  ) -> ProjectConfig:
79
86
  """Initialize project for MCP Vector Search.
80
-
87
+
81
88
  Args:
82
89
  file_extensions: File extensions to index
83
90
  embedding_model: Embedding model to use
84
91
  similarity_threshold: Similarity threshold for search
85
92
  force: Force re-initialization if already exists
86
-
93
+
87
94
  Returns:
88
95
  Project configuration
89
-
96
+
90
97
  Raises:
91
98
  ProjectInitializationError: If initialization fails
92
99
  """
@@ -99,11 +106,13 @@ class ProjectManager:
99
106
  # Create index directory
100
107
  index_path = get_default_index_path(self.project_root)
101
108
  index_path.mkdir(parents=True, exist_ok=True)
102
-
109
+
103
110
  # Detect languages and files
104
111
  detected_languages = self.detect_languages()
105
- file_count = self.count_indexable_files(file_extensions or DEFAULT_FILE_EXTENSIONS)
106
-
112
+ file_count = self.count_indexable_files(
113
+ file_extensions or DEFAULT_FILE_EXTENSIONS
114
+ )
115
+
107
116
  # Create configuration
108
117
  config = ProjectConfig(
109
118
  project_root=self.project_root,
@@ -113,29 +122,31 @@ class ProjectManager:
113
122
  similarity_threshold=similarity_threshold,
114
123
  languages=detected_languages,
115
124
  )
116
-
125
+
117
126
  # Save configuration
118
127
  self.save_config(config)
119
-
128
+
120
129
  logger.info(
121
130
  f"Initialized project at {self.project_root}",
122
131
  languages=detected_languages,
123
132
  file_count=file_count,
124
133
  extensions=config.file_extensions,
125
134
  )
126
-
135
+
127
136
  self._config = config
128
137
  return config
129
-
138
+
130
139
  except Exception as e:
131
- raise ProjectInitializationError(f"Failed to initialize project: {e}") from e
140
+ raise ProjectInitializationError(
141
+ f"Failed to initialize project: {e}"
142
+ ) from e
132
143
 
133
144
  def load_config(self) -> ProjectConfig:
134
145
  """Load project configuration.
135
-
146
+
136
147
  Returns:
137
148
  Project configuration
138
-
149
+
139
150
  Raises:
140
151
  ProjectNotFoundError: If project is not initialized
141
152
  ConfigurationError: If configuration is invalid
@@ -146,45 +157,45 @@ class ProjectManager:
146
157
  )
147
158
 
148
159
  config_path = get_default_config_path(self.project_root)
149
-
160
+
150
161
  try:
151
- with open(config_path, "r") as f:
162
+ with open(config_path) as f:
152
163
  config_data = json.load(f)
153
-
164
+
154
165
  # Convert paths back to Path objects
155
166
  config_data["project_root"] = Path(config_data["project_root"])
156
167
  config_data["index_path"] = Path(config_data["index_path"])
157
-
168
+
158
169
  config = ProjectConfig(**config_data)
159
170
  self._config = config
160
171
  return config
161
-
172
+
162
173
  except Exception as e:
163
174
  raise ConfigurationError(f"Failed to load configuration: {e}") from e
164
175
 
165
176
  def save_config(self, config: ProjectConfig) -> None:
166
177
  """Save project configuration.
167
-
178
+
168
179
  Args:
169
180
  config: Project configuration to save
170
-
181
+
171
182
  Raises:
172
183
  ConfigurationError: If saving fails
173
184
  """
174
185
  config_path = get_default_config_path(self.project_root)
175
186
  config_path.parent.mkdir(parents=True, exist_ok=True)
176
-
187
+
177
188
  try:
178
189
  # Convert to JSON-serializable format
179
- config_data = config.dict()
190
+ config_data = config.model_dump()
180
191
  config_data["project_root"] = str(config.project_root)
181
192
  config_data["index_path"] = str(config.index_path)
182
-
193
+
183
194
  with open(config_path, "w") as f:
184
195
  json.dump(config_data, f, indent=2)
185
-
196
+
186
197
  logger.debug(f"Saved configuration to {config_path}")
187
-
198
+
188
199
  except Exception as e:
189
200
  raise ConfigurationError(f"Failed to save configuration: {e}") from e
190
201
 
@@ -195,27 +206,27 @@ class ProjectManager:
195
206
  self._config = self.load_config()
196
207
  return self._config
197
208
 
198
- def detect_languages(self) -> List[str]:
209
+ def detect_languages(self) -> list[str]:
199
210
  """Detect programming languages in the project.
200
-
211
+
201
212
  Returns:
202
213
  List of detected language names
203
214
  """
204
- languages: Set[str] = set()
205
-
215
+ languages: set[str] = set()
216
+
206
217
  for file_path in self._iter_source_files():
207
218
  language = get_language_from_extension(file_path.suffix)
208
219
  if language != "text":
209
220
  languages.add(language)
210
-
211
- return sorted(list(languages))
212
221
 
213
- def count_indexable_files(self, extensions: List[str]) -> int:
222
+ return sorted(languages)
223
+
224
+ def count_indexable_files(self, extensions: list[str]) -> int:
214
225
  """Count files that can be indexed.
215
-
226
+
216
227
  Args:
217
228
  extensions: File extensions to count
218
-
229
+
219
230
  Returns:
220
231
  Number of indexable files
221
232
  """
@@ -227,17 +238,17 @@ class ProjectManager:
227
238
 
228
239
  def get_project_info(self) -> ProjectInfo:
229
240
  """Get comprehensive project information.
230
-
241
+
231
242
  Returns:
232
243
  Project information
233
244
  """
234
245
  config_path = get_default_config_path(self.project_root)
235
246
  index_path = get_default_index_path(self.project_root)
236
-
247
+
237
248
  is_initialized = self.is_initialized()
238
249
  languages = []
239
250
  file_count = 0
240
-
251
+
241
252
  if is_initialized:
242
253
  try:
243
254
  config = self.config
@@ -246,7 +257,7 @@ class ProjectManager:
246
257
  except Exception:
247
258
  # Ignore errors when getting detailed info
248
259
  pass
249
-
260
+
250
261
  return ProjectInfo(
251
262
  name=self.project_root.name,
252
263
  root_path=self.project_root,
@@ -257,40 +268,44 @@ class ProjectManager:
257
268
  file_count=file_count,
258
269
  )
259
270
 
260
- def _iter_source_files(self) -> List[Path]:
271
+ def _iter_source_files(self) -> list[Path]:
261
272
  """Iterate over source files in the project.
262
-
273
+
263
274
  Returns:
264
275
  List of source file paths
265
276
  """
266
277
  files = []
267
-
278
+
268
279
  for path in self.project_root.rglob("*"):
269
280
  if not path.is_file():
270
281
  continue
271
-
282
+
272
283
  # Skip ignored patterns
273
284
  if self._should_ignore_path(path):
274
285
  continue
275
-
286
+
276
287
  files.append(path)
277
-
288
+
278
289
  return files
279
290
 
280
291
  def _should_ignore_path(self, path: Path) -> bool:
281
292
  """Check if a path should be ignored.
282
-
293
+
283
294
  Args:
284
295
  path: Path to check
285
-
296
+
286
297
  Returns:
287
298
  True if path should be ignored
288
299
  """
300
+ # First check gitignore rules if available
301
+ if self.gitignore_parser and self.gitignore_parser.is_ignored(path):
302
+ return True
303
+
289
304
  # Check if any parent directory is in ignore patterns
290
305
  for part in path.parts:
291
306
  if part in DEFAULT_IGNORE_PATTERNS:
292
307
  return True
293
-
308
+
294
309
  # Check relative path from project root
295
310
  try:
296
311
  relative_path = path.relative_to(self.project_root)
@@ -300,5 +315,5 @@ class ProjectManager:
300
315
  except ValueError:
301
316
  # Path is not relative to project root
302
317
  return True
303
-
318
+
304
319
  return False
@@ -0,0 +1,330 @@
1
+ """Scheduling utilities for automatic reindexing."""
2
+
3
+ import platform
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from loguru import logger
9
+
10
+
11
+ class SchedulerManager:
12
+ """Manages scheduled tasks for automatic reindexing."""
13
+
14
+ def __init__(self, project_root: Path):
15
+ """Initialize scheduler manager.
16
+
17
+ Args:
18
+ project_root: Project root directory
19
+ """
20
+ self.project_root = project_root
21
+ self.system = platform.system().lower()
22
+
23
+ def install_scheduled_task(
24
+ self, interval_minutes: int = 60, task_name: str | None = None
25
+ ) -> bool:
26
+ """Install a scheduled task for automatic reindexing.
27
+
28
+ Args:
29
+ interval_minutes: Interval between reindex checks in minutes
30
+ task_name: Custom task name (auto-generated if None)
31
+
32
+ Returns:
33
+ True if task was installed successfully
34
+ """
35
+ if task_name is None:
36
+ safe_path = str(self.project_root).replace("/", "_").replace("\\", "_")
37
+ task_name = f"mcp_vector_search_reindex_{safe_path}"
38
+
39
+ if self.system == "linux" or self.system == "darwin":
40
+ return self._install_cron_job(interval_minutes, task_name)
41
+ elif self.system == "windows":
42
+ return self._install_windows_task(interval_minutes, task_name)
43
+ else:
44
+ logger.error(f"Unsupported system: {self.system}")
45
+ return False
46
+
47
+ def uninstall_scheduled_task(self, task_name: str | None = None) -> bool:
48
+ """Uninstall scheduled task.
49
+
50
+ Args:
51
+ task_name: Task name to uninstall (auto-generated if None)
52
+
53
+ Returns:
54
+ True if task was uninstalled successfully
55
+ """
56
+ if task_name is None:
57
+ safe_path = str(self.project_root).replace("/", "_").replace("\\", "_")
58
+ task_name = f"mcp_vector_search_reindex_{safe_path}"
59
+
60
+ if self.system == "linux" or self.system == "darwin":
61
+ return self._uninstall_cron_job(task_name)
62
+ elif self.system == "windows":
63
+ return self._uninstall_windows_task(task_name)
64
+ else:
65
+ logger.error(f"Unsupported system: {self.system}")
66
+ return False
67
+
68
+ def _install_cron_job(self, interval_minutes: int, task_name: str) -> bool:
69
+ """Install cron job on Linux/macOS."""
70
+ try:
71
+ # Generate cron command
72
+ python_path = sys.executable
73
+ project_root = str(self.project_root)
74
+
75
+ # Create wrapper script
76
+ script_content = f'''#!/bin/bash
77
+ # MCP Vector Search Auto-Reindex - {task_name}
78
+ cd "{project_root}" || exit 1
79
+
80
+ # Check if mcp-vector-search is available
81
+ if command -v mcp-vector-search &> /dev/null; then
82
+ mcp-vector-search auto-index check --auto-reindex --max-files 10
83
+ elif [ -f "{python_path}" ]; then
84
+ "{python_path}" -m mcp_vector_search auto-index check --auto-reindex --max-files 10
85
+ else
86
+ python3 -m mcp_vector_search auto-index check --auto-reindex --max-files 10
87
+ fi
88
+ '''
89
+
90
+ # Write script to temp file
91
+ script_dir = Path.home() / ".mcp-vector-search" / "scripts"
92
+ script_dir.mkdir(parents=True, exist_ok=True)
93
+ script_file = script_dir / f"{task_name}.sh"
94
+
95
+ script_file.write_text(script_content)
96
+ script_file.chmod(0o755)
97
+
98
+ # Calculate cron schedule
99
+ if interval_minutes >= 60:
100
+ # Hourly or less frequent
101
+ hours = interval_minutes // 60
102
+ cron_schedule = f"0 */{hours} * * *"
103
+ else:
104
+ # More frequent than hourly
105
+ cron_schedule = f"*/{interval_minutes} * * * *"
106
+
107
+ # Add to crontab
108
+ cron_entry = f"{cron_schedule} {script_file} # {task_name}\n"
109
+
110
+ # Get current crontab
111
+ try:
112
+ result = subprocess.run(
113
+ ["crontab", "-l"], capture_output=True, text=True, check=True
114
+ )
115
+ current_crontab = result.stdout
116
+ except subprocess.CalledProcessError:
117
+ current_crontab = ""
118
+
119
+ # Check if entry already exists
120
+ if task_name in current_crontab:
121
+ logger.info(f"Cron job {task_name} already exists")
122
+ return True
123
+
124
+ # Add new entry
125
+ new_crontab = current_crontab + cron_entry
126
+
127
+ # Install new crontab
128
+ process = subprocess.Popen(
129
+ ["crontab", "-"], stdin=subprocess.PIPE, text=True
130
+ )
131
+ process.communicate(input=new_crontab)
132
+
133
+ if process.returncode == 0:
134
+ logger.info(f"Installed cron job: {task_name}")
135
+ logger.info(f"Schedule: every {interval_minutes} minutes")
136
+ logger.info(f"Script: {script_file}")
137
+ return True
138
+ else:
139
+ logger.error("Failed to install cron job")
140
+ return False
141
+
142
+ except Exception as e:
143
+ logger.error(f"Failed to install cron job: {e}")
144
+ return False
145
+
146
+ def _uninstall_cron_job(self, task_name: str) -> bool:
147
+ """Uninstall cron job on Linux/macOS."""
148
+ try:
149
+ # Get current crontab
150
+ try:
151
+ result = subprocess.run(
152
+ ["crontab", "-l"], capture_output=True, text=True, check=True
153
+ )
154
+ current_crontab = result.stdout
155
+ except subprocess.CalledProcessError:
156
+ logger.info("No crontab found")
157
+ return True
158
+
159
+ # Remove lines containing task name
160
+ lines = current_crontab.split("\n")
161
+ new_lines = [line for line in lines if task_name not in line]
162
+ new_crontab = "\n".join(new_lines)
163
+
164
+ # Install new crontab
165
+ if new_crontab.strip():
166
+ process = subprocess.Popen(
167
+ ["crontab", "-"], stdin=subprocess.PIPE, text=True
168
+ )
169
+ process.communicate(input=new_crontab)
170
+ else:
171
+ # Remove crontab entirely if empty
172
+ subprocess.run(["crontab", "-r"], check=False)
173
+
174
+ # Remove script file
175
+ script_dir = Path.home() / ".mcp-vector-search" / "scripts"
176
+ script_file = script_dir / f"{task_name}.sh"
177
+ if script_file.exists():
178
+ script_file.unlink()
179
+
180
+ logger.info(f"Uninstalled cron job: {task_name}")
181
+ return True
182
+
183
+ except Exception as e:
184
+ logger.error(f"Failed to uninstall cron job: {e}")
185
+ return False
186
+
187
+ def _install_windows_task(self, interval_minutes: int, task_name: str) -> bool:
188
+ """Install Windows scheduled task."""
189
+ try:
190
+ python_path = sys.executable
191
+ project_root = str(self.project_root)
192
+
193
+ # Create PowerShell script
194
+ script_content = f'''# MCP Vector Search Auto-Reindex - {task_name}
195
+ Set-Location "{project_root}"
196
+
197
+ try {{
198
+ if (Get-Command "mcp-vector-search" -ErrorAction SilentlyContinue) {{
199
+ mcp-vector-search auto-index check --auto-reindex --max-files 10
200
+ }} elseif (Test-Path "{python_path}") {{
201
+ & "{python_path}" -m mcp_vector_search auto-index check --auto-reindex --max-files 10
202
+ }} else {{
203
+ python -m mcp_vector_search auto-index check --auto-reindex --max-files 10
204
+ }}
205
+ }} catch {{
206
+ # Silently ignore errors
207
+ }}
208
+ '''
209
+
210
+ # Write script
211
+ script_dir = Path.home() / ".mcp-vector-search" / "scripts"
212
+ script_dir.mkdir(parents=True, exist_ok=True)
213
+ script_file = script_dir / f"{task_name}.ps1"
214
+
215
+ script_file.write_text(script_content)
216
+
217
+ # Create scheduled task using schtasks
218
+ cmd = [
219
+ "schtasks",
220
+ "/create",
221
+ "/tn",
222
+ task_name,
223
+ "/tr",
224
+ f'powershell.exe -ExecutionPolicy Bypass -File "{script_file}"',
225
+ "/sc",
226
+ "minute",
227
+ "/mo",
228
+ str(interval_minutes),
229
+ "/f", # Force overwrite if exists
230
+ ]
231
+
232
+ result = subprocess.run(cmd, capture_output=True, text=True)
233
+
234
+ if result.returncode == 0:
235
+ logger.info(f"Installed Windows task: {task_name}")
236
+ logger.info(f"Schedule: every {interval_minutes} minutes")
237
+ return True
238
+ else:
239
+ logger.error(f"Failed to install Windows task: {result.stderr}")
240
+ return False
241
+
242
+ except Exception as e:
243
+ logger.error(f"Failed to install Windows task: {e}")
244
+ return False
245
+
246
+ def _uninstall_windows_task(self, task_name: str) -> bool:
247
+ """Uninstall Windows scheduled task."""
248
+ try:
249
+ # Delete scheduled task
250
+ cmd = ["schtasks", "/delete", "/tn", task_name, "/f"]
251
+ result = subprocess.run(cmd, capture_output=True, text=True)
252
+
253
+ # Remove script file
254
+ script_dir = Path.home() / ".mcp-vector-search" / "scripts"
255
+ script_file = script_dir / f"{task_name}.ps1"
256
+ if script_file.exists():
257
+ script_file.unlink()
258
+
259
+ if result.returncode == 0:
260
+ logger.info(f"Uninstalled Windows task: {task_name}")
261
+ return True
262
+ else:
263
+ # Task might not exist, which is fine
264
+ logger.info(
265
+ f"Windows task {task_name} was not found (already uninstalled)"
266
+ )
267
+ return True
268
+
269
+ except Exception as e:
270
+ logger.error(f"Failed to uninstall Windows task: {e}")
271
+ return False
272
+
273
+ def get_scheduled_task_status(self, task_name: str | None = None) -> dict:
274
+ """Get status of scheduled tasks.
275
+
276
+ Args:
277
+ task_name: Task name to check (auto-generated if None)
278
+
279
+ Returns:
280
+ Dictionary with task status information
281
+ """
282
+ if task_name is None:
283
+ safe_path = str(self.project_root).replace("/", "_").replace("\\", "_")
284
+ task_name = f"mcp_vector_search_reindex_{safe_path}"
285
+
286
+ status = {
287
+ "system": self.system,
288
+ "task_name": task_name,
289
+ "exists": False,
290
+ "enabled": False,
291
+ "last_run": None,
292
+ "next_run": None,
293
+ }
294
+
295
+ if self.system == "linux" or self.system == "darwin":
296
+ status.update(self._get_cron_status(task_name))
297
+ elif self.system == "windows":
298
+ status.update(self._get_windows_task_status(task_name))
299
+
300
+ return status
301
+
302
+ def _get_cron_status(self, task_name: str) -> dict:
303
+ """Get cron job status."""
304
+ try:
305
+ result = subprocess.run(
306
+ ["crontab", "-l"], capture_output=True, text=True, check=True
307
+ )
308
+
309
+ exists = task_name in result.stdout
310
+ return {"exists": exists, "enabled": exists}
311
+
312
+ except subprocess.CalledProcessError:
313
+ return {"exists": False, "enabled": False}
314
+
315
+ def _get_windows_task_status(self, task_name: str) -> dict:
316
+ """Get Windows task status."""
317
+ try:
318
+ result = subprocess.run(
319
+ ["schtasks", "/query", "/tn", task_name], capture_output=True, text=True
320
+ )
321
+
322
+ if result.returncode == 0:
323
+ # Parse output for status
324
+ enabled = "Ready" in result.stdout or "Running" in result.stdout
325
+ return {"exists": True, "enabled": enabled}
326
+ else:
327
+ return {"exists": False, "enabled": False}
328
+
329
+ except Exception:
330
+ return {"exists": False, "enabled": False}