tree-sitter-analyzer 0.9.9__py3-none-any.whl → 1.1.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.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (28) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +11 -2
  3. tree_sitter_analyzer/cli/commands/base_command.py +2 -2
  4. tree_sitter_analyzer/cli/commands/partial_read_command.py +2 -2
  5. tree_sitter_analyzer/cli/info_commands.py +7 -4
  6. tree_sitter_analyzer/cli_main.py +1 -2
  7. tree_sitter_analyzer/core/analysis_engine.py +2 -2
  8. tree_sitter_analyzer/core/query_service.py +162 -162
  9. tree_sitter_analyzer/file_handler.py +6 -4
  10. tree_sitter_analyzer/formatters/base_formatter.py +6 -4
  11. tree_sitter_analyzer/mcp/__init__.py +1 -1
  12. tree_sitter_analyzer/mcp/resources/__init__.py +1 -1
  13. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +16 -5
  14. tree_sitter_analyzer/mcp/server.py +16 -11
  15. tree_sitter_analyzer/mcp/tools/__init__.py +1 -1
  16. tree_sitter_analyzer/mcp/utils/__init__.py +2 -2
  17. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -569
  18. tree_sitter_analyzer/mcp/utils/path_resolver.py +240 -20
  19. tree_sitter_analyzer/output_manager.py +7 -3
  20. tree_sitter_analyzer/project_detector.py +27 -27
  21. tree_sitter_analyzer/security/boundary_manager.py +37 -28
  22. tree_sitter_analyzer/security/validator.py +23 -12
  23. tree_sitter_analyzer/table_formatter.py +6 -4
  24. tree_sitter_analyzer/utils.py +1 -2
  25. {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/METADATA +38 -12
  26. {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/RECORD +28 -28
  27. {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/WHEEL +0 -0
  28. {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "0.9.9"
14
+ __version__ = "1.1.0"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -126,11 +126,20 @@ def analyze_file(
126
126
  except FileNotFoundError as e:
127
127
  # Re-raise FileNotFoundError for tests that expect it
128
128
  raise e
129
+ except (ValueError, TypeError, OSError) as e:
130
+ # Handle specific expected errors
131
+ log_error(f"API analyze_file failed with {type(e).__name__}: {e}")
132
+ return {
133
+ "success": False,
134
+ "error": f"{type(e).__name__}: {str(e)}",
135
+ "file_info": {"path": str(file_path), "exists": Path(file_path).exists()},
136
+ }
129
137
  except Exception as e:
130
- log_error(f"API analyze_file failed: {e}")
138
+ # Handle unexpected errors
139
+ log_error(f"API analyze_file failed with unexpected error: {e}")
131
140
  return {
132
141
  "success": False,
133
- "error": str(e),
142
+ "error": f"Unexpected error: {str(e)}",
134
143
  "file_info": {"path": str(file_path), "exists": Path(file_path).exists()},
135
144
  }
136
145
 
@@ -54,9 +54,9 @@ class BaseCommand(ABC):
54
54
  output_error(f"Invalid file path: {error_msg}")
55
55
  return False
56
56
 
57
- import os
57
+ from pathlib import Path
58
58
 
59
- if not os.path.exists(self.args.file_path):
59
+ if not Path(self.args.file_path).exists():
60
60
  output_error("Invalid file path: file does not exist")
61
61
  return False
62
62
 
@@ -31,9 +31,9 @@ class PartialReadCommand(BaseCommand):
31
31
  output_error("File path not specified.")
32
32
  return False
33
33
 
34
- import os
34
+ from pathlib import Path
35
35
 
36
- if not os.path.exists(self.args.file_path):
36
+ if not Path(self.args.file_path).exists():
37
37
  from ...output_manager import output_error
38
38
 
39
39
  output_error(f"File not found: {self.args.file_path}")
@@ -112,10 +112,13 @@ class ShowExtensionsCommand(InfoCommand):
112
112
  def execute(self) -> int:
113
113
  output_list("Supported file extensions:")
114
114
  supported_extensions = detector.get_supported_extensions()
115
- for i in range(0, len(supported_extensions), 8):
116
- line = " " + " ".join(
117
- f"{ext:<6}" for ext in supported_extensions[i : i + 8]
118
- )
115
+ # Use more efficient chunking with itertools.islice
116
+ from itertools import islice
117
+
118
+ chunk_size = 8
119
+ for i in range(0, len(supported_extensions), chunk_size):
120
+ chunk = list(islice(supported_extensions, i, i + chunk_size))
121
+ line = " " + " ".join(f"{ext:<6}" for ext in chunk)
119
122
  output_list(line)
120
123
  output_info(f"\nTotal {len(supported_extensions)} extensions supported")
121
124
  return 0
@@ -3,6 +3,7 @@
3
3
 
4
4
  import argparse
5
5
  import logging
6
+ import os
6
7
  import sys
7
8
  from typing import Any
8
9
 
@@ -268,8 +269,6 @@ def handle_special_commands(args: argparse.Namespace) -> int | None:
268
269
  def main() -> None:
269
270
  """Main entry point for the CLI."""
270
271
  # Early check for quiet mode to set environment variable before any imports
271
- import os
272
-
273
272
  if "--quiet" in sys.argv:
274
273
  os.environ["LOG_LEVEL"] = "ERROR"
275
274
  else:
@@ -395,9 +395,9 @@ class UnifiedAnalysisEngine:
395
395
  Detected language name
396
396
  """
397
397
  # 簡易的な拡張子ベース検出
398
- import os
398
+ from pathlib import Path
399
399
 
400
- _, ext = os.path.splitext(file_path)
400
+ ext = Path(file_path).suffix
401
401
 
402
402
  language_map = {
403
403
  ".java": "java",
@@ -1,162 +1,162 @@
1
- #!/usr/bin/env python3
2
- """
3
- Query Service
4
-
5
- Unified query service for both CLI and MCP interfaces to avoid code duplication.
6
- Provides core tree-sitter query functionality including predefined and custom queries.
7
- """
8
-
9
- import logging
10
- from typing import Any
11
-
12
- from ..encoding_utils import read_file_safe
13
- from ..query_loader import query_loader
14
- from .parser import Parser
15
- from .query_filter import QueryFilter
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class QueryService:
21
- """Unified query service providing tree-sitter query functionality"""
22
-
23
- def __init__(self, project_root: str | None = None) -> None:
24
- """Initialize the query service"""
25
- self.project_root = project_root
26
- self.parser = Parser()
27
- self.filter = QueryFilter()
28
-
29
- async def execute_query(
30
- self,
31
- file_path: str,
32
- language: str,
33
- query_key: str | None = None,
34
- query_string: str | None = None,
35
- filter_expression: str | None = None,
36
- ) -> list[dict[str, Any]] | None:
37
- """
38
- Execute a query
39
-
40
- Args:
41
- file_path: Path to the file to analyze
42
- language: Programming language
43
- query_key: Predefined query key (e.g., 'methods', 'class')
44
- query_string: Custom query string (e.g., '(method_declaration) @method')
45
- filter_expression: Filter expression (e.g., 'name=main', 'name=~get*,public=true')
46
-
47
- Returns:
48
- List of query results, each containing capture_name, node_type, start_line, end_line, content
49
-
50
- Raises:
51
- ValueError: If neither query_key nor query_string is provided
52
- FileNotFoundError: If file doesn't exist
53
- Exception: If query execution fails
54
- """
55
- if not query_key and not query_string:
56
- raise ValueError("Must provide either query_key or query_string")
57
-
58
- if query_key and query_string:
59
- raise ValueError("Cannot provide both query_key and query_string")
60
-
61
- try:
62
- # Read file content
63
- content, encoding = read_file_safe(file_path)
64
-
65
- # Parse file
66
- parse_result = self.parser.parse_code(content, language, file_path)
67
- if not parse_result or not parse_result.tree:
68
- raise Exception("Failed to parse file")
69
-
70
- tree = parse_result.tree
71
- language_obj = tree.language if hasattr(tree, "language") else None
72
- if not language_obj:
73
- raise Exception(f"Language object not available for {language}")
74
-
75
- # Get query string
76
- if query_key:
77
- query_string = query_loader.get_query(language, query_key)
78
- if not query_string:
79
- raise ValueError(
80
- f"Query '{query_key}' not found for language '{language}'"
81
- )
82
-
83
- # Execute tree-sitter query
84
- ts_query = language_obj.query(query_string)
85
- captures = ts_query.captures(tree.root_node)
86
-
87
- # Process capture results
88
- results = []
89
- if isinstance(captures, dict):
90
- # New tree-sitter API returns dictionary
91
- for capture_name, nodes in captures.items():
92
- for node in nodes:
93
- results.append(self._create_result_dict(node, capture_name))
94
- else:
95
- # Old tree-sitter API returns list of tuples
96
- for capture in captures:
97
- if isinstance(capture, tuple) and len(capture) == 2:
98
- node, name = capture
99
- results.append(self._create_result_dict(node, name))
100
-
101
- # Apply filters
102
- if filter_expression and results:
103
- results = self.filter.filter_results(results, filter_expression)
104
-
105
- return results
106
-
107
- except Exception as e:
108
- logger.error(f"Query execution failed: {e}")
109
- raise
110
-
111
- def _create_result_dict(self, node: Any, capture_name: str) -> dict[str, Any]:
112
- """
113
- Create result dictionary from tree-sitter node
114
-
115
- Args:
116
- node: tree-sitter node
117
- capture_name: capture name
118
-
119
- Returns:
120
- Result dictionary
121
- """
122
- return {
123
- "capture_name": capture_name,
124
- "node_type": node.type if hasattr(node, "type") else "unknown",
125
- "start_line": (
126
- node.start_point[0] + 1 if hasattr(node, "start_point") else 0
127
- ),
128
- "end_line": node.end_point[0] + 1 if hasattr(node, "end_point") else 0,
129
- "content": (
130
- node.text.decode("utf-8", errors="replace")
131
- if hasattr(node, "text") and node.text
132
- else ""
133
- ),
134
- }
135
-
136
- def get_available_queries(self, language: str) -> list[str]:
137
- """
138
- Get available query keys for specified language
139
-
140
- Args:
141
- language: Programming language
142
-
143
- Returns:
144
- List of available query keys
145
- """
146
- return query_loader.list_queries(language)
147
-
148
- def get_query_description(self, language: str, query_key: str) -> str | None:
149
- """
150
- Get description for query key
151
-
152
- Args:
153
- language: Programming language
154
- query_key: Query key
155
-
156
- Returns:
157
- Query description, or None if not found
158
- """
159
- try:
160
- return query_loader.get_query_description(language, query_key)
161
- except Exception:
162
- return None
1
+ #!/usr/bin/env python3
2
+ """
3
+ Query Service
4
+
5
+ Unified query service for both CLI and MCP interfaces to avoid code duplication.
6
+ Provides core tree-sitter query functionality including predefined and custom queries.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ..encoding_utils import read_file_safe
13
+ from ..query_loader import query_loader
14
+ from .parser import Parser
15
+ from .query_filter import QueryFilter
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class QueryService:
21
+ """Unified query service providing tree-sitter query functionality"""
22
+
23
+ def __init__(self, project_root: str | None = None) -> None:
24
+ """Initialize the query service"""
25
+ self.project_root = project_root
26
+ self.parser = Parser()
27
+ self.filter = QueryFilter()
28
+
29
+ async def execute_query(
30
+ self,
31
+ file_path: str,
32
+ language: str,
33
+ query_key: str | None = None,
34
+ query_string: str | None = None,
35
+ filter_expression: str | None = None,
36
+ ) -> list[dict[str, Any]] | None:
37
+ """
38
+ Execute a query
39
+
40
+ Args:
41
+ file_path: Path to the file to analyze
42
+ language: Programming language
43
+ query_key: Predefined query key (e.g., 'methods', 'class')
44
+ query_string: Custom query string (e.g., '(method_declaration) @method')
45
+ filter_expression: Filter expression (e.g., 'name=main', 'name=~get*,public=true')
46
+
47
+ Returns:
48
+ List of query results, each containing capture_name, node_type, start_line, end_line, content
49
+
50
+ Raises:
51
+ ValueError: If neither query_key nor query_string is provided
52
+ FileNotFoundError: If file doesn't exist
53
+ Exception: If query execution fails
54
+ """
55
+ if not query_key and not query_string:
56
+ raise ValueError("Must provide either query_key or query_string")
57
+
58
+ if query_key and query_string:
59
+ raise ValueError("Cannot provide both query_key and query_string")
60
+
61
+ try:
62
+ # Read file content
63
+ content, encoding = read_file_safe(file_path)
64
+
65
+ # Parse file
66
+ parse_result = self.parser.parse_code(content, language, file_path)
67
+ if not parse_result or not parse_result.tree:
68
+ raise Exception("Failed to parse file")
69
+
70
+ tree = parse_result.tree
71
+ language_obj = tree.language if hasattr(tree, "language") else None
72
+ if not language_obj:
73
+ raise Exception(f"Language object not available for {language}")
74
+
75
+ # Get query string
76
+ if query_key:
77
+ query_string = query_loader.get_query(language, query_key)
78
+ if not query_string:
79
+ raise ValueError(
80
+ f"Query '{query_key}' not found for language '{language}'"
81
+ )
82
+
83
+ # Execute tree-sitter query
84
+ ts_query = language_obj.query(query_string)
85
+ captures = ts_query.captures(tree.root_node)
86
+
87
+ # Process capture results
88
+ results = []
89
+ if isinstance(captures, dict):
90
+ # New tree-sitter API returns dictionary
91
+ for capture_name, nodes in captures.items():
92
+ for node in nodes:
93
+ results.append(self._create_result_dict(node, capture_name))
94
+ else:
95
+ # Old tree-sitter API returns list of tuples
96
+ for capture in captures:
97
+ if isinstance(capture, tuple) and len(capture) == 2:
98
+ node, name = capture
99
+ results.append(self._create_result_dict(node, name))
100
+
101
+ # Apply filters
102
+ if filter_expression and results:
103
+ results = self.filter.filter_results(results, filter_expression)
104
+
105
+ return results
106
+
107
+ except Exception as e:
108
+ logger.error(f"Query execution failed: {e}")
109
+ raise
110
+
111
+ def _create_result_dict(self, node: Any, capture_name: str) -> dict[str, Any]:
112
+ """
113
+ Create result dictionary from tree-sitter node
114
+
115
+ Args:
116
+ node: tree-sitter node
117
+ capture_name: capture name
118
+
119
+ Returns:
120
+ Result dictionary
121
+ """
122
+ return {
123
+ "capture_name": capture_name,
124
+ "node_type": node.type if hasattr(node, "type") else "unknown",
125
+ "start_line": (
126
+ node.start_point[0] + 1 if hasattr(node, "start_point") else 0
127
+ ),
128
+ "end_line": node.end_point[0] + 1 if hasattr(node, "end_point") else 0,
129
+ "content": (
130
+ node.text.decode("utf-8", errors="replace")
131
+ if hasattr(node, "text") and node.text
132
+ else ""
133
+ ),
134
+ }
135
+
136
+ def get_available_queries(self, language: str) -> list[str]:
137
+ """
138
+ Get available query keys for specified language
139
+
140
+ Args:
141
+ language: Programming language
142
+
143
+ Returns:
144
+ List of available query keys
145
+ """
146
+ return query_loader.list_queries(language)
147
+
148
+ def get_query_description(self, language: str, query_key: str) -> str | None:
149
+ """
150
+ Get description for query key
151
+
152
+ Args:
153
+ language: Programming language
154
+ query_key: Query key
155
+
156
+ Returns:
157
+ Query description, or None if not found
158
+ """
159
+ try:
160
+ return query_loader.get_query_description(language, query_key)
161
+ except Exception:
162
+ return None
@@ -5,7 +5,7 @@ File Handler Module
5
5
  This module provides file reading functionality with encoding detection and fallback.
6
6
  """
7
7
 
8
- import os
8
+ from pathlib import Path
9
9
 
10
10
  from .encoding_utils import read_file_safe
11
11
  from .utils import log_error, log_info, log_warning
@@ -21,7 +21,7 @@ def detect_language_from_extension(file_path: str) -> str:
21
21
  Returns:
22
22
  Language name or 'unknown' if not recognized
23
23
  """
24
- extension = os.path.splitext(file_path)[1].lower()
24
+ extension = Path(file_path).suffix.lower()
25
25
 
26
26
  extension_map = {
27
27
  ".java": "java",
@@ -56,7 +56,8 @@ def read_file_with_fallback(file_path: str) -> bytes | None:
56
56
  File content as bytes, or None if file doesn't exist
57
57
  """
58
58
  # Check file existence first
59
- if not os.path.exists(file_path):
59
+ file_obj = Path(file_path)
60
+ if not file_obj.exists():
60
61
  log_error(f"File does not exist: {file_path}")
61
62
  return None
62
63
 
@@ -93,7 +94,8 @@ def read_file_partial(
93
94
  Selected content string, or None on error
94
95
  """
95
96
  # Check file existence
96
- if not os.path.exists(file_path):
97
+ file_obj = Path(file_path)
98
+ if not file_obj.exists():
97
99
  log_error(f"File does not exist: {file_path}")
98
100
  return None
99
101
 
@@ -5,7 +5,6 @@ Base formatter for language-specific table formatting.
5
5
 
6
6
  import csv
7
7
  import io
8
- import os
9
8
  from abc import ABC, abstractmethod
10
9
  from typing import Any
11
10
 
@@ -18,12 +17,15 @@ class BaseTableFormatter(ABC):
18
17
 
19
18
  def _get_platform_newline(self) -> str:
20
19
  """Get platform-specific newline code"""
21
- return os.linesep
20
+ import os
21
+
22
+ return "\r\n" if os.name == "nt" else "\n" # Windows uses \r\n, others use \n
22
23
 
23
24
  def _convert_to_platform_newlines(self, text: str) -> str:
24
25
  """Convert regular \n to platform-specific newline code"""
25
- if os.linesep != "\n":
26
- return text.replace("\n", os.linesep)
26
+ platform_newline = self._get_platform_newline()
27
+ if platform_newline != "\n":
28
+ return text.replace("\n", platform_newline)
27
29
  return text
28
30
 
29
31
  def format_structure(self, structure_data: dict[str, Any]) -> str:
@@ -15,7 +15,7 @@ try:
15
15
  __version__ = main_version
16
16
  except ImportError:
17
17
  # Fallback version if main package not available
18
- __version__ = "0.9.1"
18
+ __version__ = "1.1.0"
19
19
 
20
20
  __author__ = "Tree-sitter Analyzer Team"
21
21
 
@@ -19,7 +19,7 @@ from .code_file_resource import CodeFileResource
19
19
  from .project_stats_resource import ProjectStatsResource
20
20
 
21
21
  # Resource metadata
22
- __version__ = "1.0.0"
22
+ __version__ = "1.1.0"
23
23
  __author__ = "Tree-Sitter Analyzer Team"
24
24
 
25
25
  # MCP Resource capabilities
@@ -123,8 +123,8 @@ class ProjectStatsResource:
123
123
 
124
124
  self._project_path = project_path
125
125
 
126
- # Initialize analyzers with the project path
127
- self._advanced_analyzer = get_analysis_engine()
126
+ # Note: analysis_engine is already initialized in __init__
127
+ # No need to reinitialize here
128
128
 
129
129
  logger.debug(f"Set project path to: {project_path}")
130
130
 
@@ -336,16 +336,27 @@ class ProjectStatsResource:
336
336
 
337
337
  # Use appropriate analyzer based on language
338
338
  if language == "java":
339
- # Use advanced analyzer for Java
340
- # 使用 await 调用异步方法
341
- file_analysis = await self._advanced_analyzer.analyze_file(
339
+ # Use analysis engine for Java
340
+ file_analysis = await self.analysis_engine.analyze_file(
342
341
  str(file_path)
343
342
  )
344
343
  if file_analysis and hasattr(file_analysis, "methods"):
344
+ # Extract complexity from methods if available
345
345
  complexity = sum(
346
346
  method.complexity_score or 0
347
347
  for method in file_analysis.methods
348
348
  )
349
+ elif file_analysis and hasattr(file_analysis, "elements"):
350
+ # Extract complexity from elements for new architecture
351
+ methods = [
352
+ e
353
+ for e in file_analysis.elements
354
+ if hasattr(e, "complexity_score")
355
+ ]
356
+ complexity = sum(
357
+ getattr(method, "complexity_score", 0) or 0
358
+ for method in methods
359
+ )
349
360
  else:
350
361
  complexity = 0
351
362
  else:
@@ -11,6 +11,7 @@ import asyncio
11
11
  import json
12
12
  import os
13
13
  import sys
14
+ from pathlib import Path as PathClass
14
15
  from typing import Any
15
16
 
16
17
  try:
@@ -141,15 +142,13 @@ class TreeSitterAnalyzerMCPServer:
141
142
  include_details = arguments.get("include_details", False)
142
143
 
143
144
  # Resolve relative path against project root for consistent behavior
144
- import os
145
-
146
145
  base_root = getattr(
147
146
  getattr(self.security_validator, "boundary_manager", None),
148
147
  "project_root",
149
148
  None,
150
149
  )
151
- if not os.path.isabs(file_path) and base_root:
152
- resolved_path = os.path.realpath(os.path.join(base_root, file_path))
150
+ if not PathClass(file_path).is_absolute() and base_root:
151
+ resolved_path = str((PathClass(base_root) / file_path).resolve())
153
152
  else:
154
153
  resolved_path = file_path
155
154
 
@@ -159,13 +158,11 @@ class TreeSitterAnalyzerMCPServer:
159
158
  raise ValueError(f"Invalid file path: {error_msg}")
160
159
 
161
160
  # Use analysis engine directly
162
- from pathlib import Path
163
-
164
161
  from ..core.analysis_engine import AnalysisRequest
165
162
  from ..language_detector import detect_language_from_file
166
163
 
167
164
  # Validate file exists
168
- if not Path(resolved_path).exists():
165
+ if not PathClass(resolved_path).exists():
169
166
  raise FileNotFoundError(f"File not found: {file_path}")
170
167
 
171
168
  # Detect language if not specified
@@ -404,7 +401,7 @@ class TreeSitterAnalyzerMCPServer:
404
401
  raise ValueError(
405
402
  "project_path parameter is required and must be a string"
406
403
  )
407
- if not os.path.isdir(project_path):
404
+ if not PathClass(project_path).is_dir():
408
405
  raise ValueError(f"Project path does not exist: {project_path}")
409
406
  self.set_project_path(project_path)
410
407
  result = {"status": "success", "project_root": project_path}
@@ -596,8 +593,12 @@ async def main() -> None:
596
593
  if args.project_root:
597
594
  project_root = args.project_root
598
595
  # Priority 2: Environment variable
599
- elif os.getenv("TREE_SITTER_PROJECT_ROOT"):
600
- project_root = os.getenv("TREE_SITTER_PROJECT_ROOT")
596
+ elif (
597
+ PathClass.cwd()
598
+ .joinpath(os.environ.get("TREE_SITTER_PROJECT_ROOT", ""))
599
+ .exists()
600
+ ):
601
+ project_root = os.environ.get("TREE_SITTER_PROJECT_ROOT")
601
602
  # Priority 3: Auto-detection from current directory
602
603
  else:
603
604
  project_root = detect_project_root()
@@ -608,7 +609,11 @@ async def main() -> None:
608
609
  )
609
610
 
610
611
  # Validate existence; if invalid, fall back to auto-detected root
611
- if not project_root or invalid_placeholder or not os.path.isdir(project_root):
612
+ if (
613
+ not project_root
614
+ or invalid_placeholder
615
+ or not PathClass(project_root).is_dir()
616
+ ):
612
617
  detected = detect_project_root()
613
618
  try:
614
619
  logger.warning(
@@ -8,7 +8,7 @@ through the Model Context Protocol.
8
8
 
9
9
  from typing import Any
10
10
 
11
- __version__ = "1.0.0"
11
+ __version__ = "1.1.0"
12
12
 
13
13
  # Tool registry for easy access
14
14
  AVAILABLE_TOOLS: dict[str, dict[str, Any]] = {