tree-sitter-analyzer 1.9.17.1__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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
list_files MCP Tool (fd wrapper)
|
|
4
|
+
|
|
5
|
+
Safely list files/directories based on name patterns and constraints, using fd.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..utils.error_handler import handle_mcp_errors
|
|
16
|
+
from ..utils.file_output_manager import FileOutputManager
|
|
17
|
+
from ..utils.gitignore_detector import get_default_detector
|
|
18
|
+
from . import fd_rg_utils
|
|
19
|
+
from .base_tool import BaseMCPTool
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ListFilesTool(BaseMCPTool):
|
|
25
|
+
"""MCP tool that wraps fd to list files with safety limits."""
|
|
26
|
+
|
|
27
|
+
def get_tool_definition(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"name": "list_files",
|
|
30
|
+
"description": "List files and directories using fd with advanced filtering options. Supports glob patterns, file types, size filters, and more. Returns file paths with metadata or just counts, with optional file output and token optimization.",
|
|
31
|
+
"inputSchema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"roots": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {"type": "string"},
|
|
37
|
+
"description": "Directory paths to search in. Must be within project boundaries for security. Example: ['.', 'src/', '/path/to/dir']",
|
|
38
|
+
},
|
|
39
|
+
"pattern": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Search pattern for file/directory names. Use with 'glob' for shell patterns or regex. Example: '*.py', 'test_*', 'main.js'",
|
|
42
|
+
},
|
|
43
|
+
"glob": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"default": False,
|
|
46
|
+
"description": "Treat pattern as glob (shell wildcard) instead of regex. True for '*.py', False for '.*\\.py$'",
|
|
47
|
+
},
|
|
48
|
+
"types": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"items": {"type": "string"},
|
|
51
|
+
"description": "File types to include. Values: 'f'=files, 'd'=directories, 'l'=symlinks, 'x'=executable, 'e'=empty. Example: ['f'] for files only",
|
|
52
|
+
},
|
|
53
|
+
"extensions": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {"type": "string"},
|
|
56
|
+
"description": "File extensions to include (without dots). Example: ['py', 'js', 'md'] for Python, JavaScript, and Markdown files",
|
|
57
|
+
},
|
|
58
|
+
"exclude": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {"type": "string"},
|
|
61
|
+
"description": "Patterns to exclude from results. Example: ['*.tmp', '__pycache__', 'node_modules'] to skip temporary and cache files",
|
|
62
|
+
},
|
|
63
|
+
"depth": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"description": "Maximum directory depth to search. 1=current level only, 2=one level deep, etc. Useful to avoid deep recursion",
|
|
66
|
+
},
|
|
67
|
+
"follow_symlinks": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"default": False,
|
|
70
|
+
"description": "Follow symbolic links during search. False=skip symlinks (safer), True=follow them (may cause loops)",
|
|
71
|
+
},
|
|
72
|
+
"hidden": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"default": False,
|
|
75
|
+
"description": "Include hidden files/directories (starting with dot). False=skip .git, .env, True=include all",
|
|
76
|
+
},
|
|
77
|
+
"no_ignore": {
|
|
78
|
+
"type": "boolean",
|
|
79
|
+
"default": False,
|
|
80
|
+
"description": "Ignore .gitignore and similar files. False=respect ignore files, True=search everything",
|
|
81
|
+
},
|
|
82
|
+
"size": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": {"type": "string"},
|
|
85
|
+
"description": "File size filters. Format: '+10M'=larger than 10MB, '-1K'=smaller than 1KB, '100B'=exactly 100 bytes. Units: B, K, M, G",
|
|
86
|
+
},
|
|
87
|
+
"changed_within": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Files modified within timeframe. Format: '1d'=1 day, '2h'=2 hours, '30m'=30 minutes, '1w'=1 week",
|
|
90
|
+
},
|
|
91
|
+
"changed_before": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Files modified before timeframe. Same format as changed_within. Useful for finding old files",
|
|
94
|
+
},
|
|
95
|
+
"full_path_match": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"default": False,
|
|
98
|
+
"description": "Match pattern against full path instead of just filename. True for 'src/main.py', False for 'main.py'",
|
|
99
|
+
},
|
|
100
|
+
"absolute": {
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"default": True,
|
|
103
|
+
"description": "Return absolute paths. True='/full/path/file.py', False='./file.py'. Absolute paths are more reliable",
|
|
104
|
+
},
|
|
105
|
+
"limit": {
|
|
106
|
+
"type": "integer",
|
|
107
|
+
"description": "Maximum number of results to return. Default 2000, max 10000. Use to prevent overwhelming output",
|
|
108
|
+
},
|
|
109
|
+
"count_only": {
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"default": False,
|
|
112
|
+
"description": "Return only the total count of matching files instead of file details. Useful for quick statistics",
|
|
113
|
+
},
|
|
114
|
+
"output_file": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Optional filename to save output to file (extension auto-detected based on content)",
|
|
117
|
+
},
|
|
118
|
+
"suppress_output": {
|
|
119
|
+
"type": "boolean",
|
|
120
|
+
"description": "When true and output_file is specified, suppress detailed output in response to save tokens",
|
|
121
|
+
"default": False,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
"required": ["roots"],
|
|
125
|
+
"additionalProperties": False,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def _validate_roots(self, roots: list[str]) -> list[str]:
|
|
130
|
+
if not roots or not isinstance(roots, list):
|
|
131
|
+
raise ValueError("roots must be a non-empty array of strings")
|
|
132
|
+
validated: list[str] = []
|
|
133
|
+
for r in roots:
|
|
134
|
+
if not isinstance(r, str) or not r.strip():
|
|
135
|
+
raise ValueError("root entries must be non-empty strings")
|
|
136
|
+
# Resolve and enforce boundary
|
|
137
|
+
resolved = self.path_resolver.resolve(r)
|
|
138
|
+
is_valid, error = self.security_validator.validate_directory_path(
|
|
139
|
+
resolved, must_exist=True
|
|
140
|
+
)
|
|
141
|
+
if not is_valid:
|
|
142
|
+
raise ValueError(f"Invalid root '{r}': {error}")
|
|
143
|
+
validated.append(resolved)
|
|
144
|
+
return validated
|
|
145
|
+
|
|
146
|
+
def validate_arguments(self, arguments: dict[str, Any]) -> bool:
|
|
147
|
+
if "roots" not in arguments:
|
|
148
|
+
raise ValueError("roots is required")
|
|
149
|
+
roots = arguments["roots"]
|
|
150
|
+
if not isinstance(roots, list):
|
|
151
|
+
raise ValueError("roots must be an array")
|
|
152
|
+
# Basic type checks for optional fields
|
|
153
|
+
for key in [
|
|
154
|
+
"pattern",
|
|
155
|
+
"changed_within",
|
|
156
|
+
"changed_before",
|
|
157
|
+
]:
|
|
158
|
+
if key in arguments and not isinstance(arguments[key], str):
|
|
159
|
+
raise ValueError(f"{key} must be a string")
|
|
160
|
+
for key in [
|
|
161
|
+
"glob",
|
|
162
|
+
"follow_symlinks",
|
|
163
|
+
"hidden",
|
|
164
|
+
"no_ignore",
|
|
165
|
+
"full_path_match",
|
|
166
|
+
"absolute",
|
|
167
|
+
]:
|
|
168
|
+
if key in arguments and not isinstance(arguments[key], bool):
|
|
169
|
+
raise ValueError(f"{key} must be a boolean")
|
|
170
|
+
if "depth" in arguments and not isinstance(arguments["depth"], int):
|
|
171
|
+
raise ValueError("depth must be an integer")
|
|
172
|
+
if "limit" in arguments and not isinstance(arguments["limit"], int):
|
|
173
|
+
raise ValueError("limit must be an integer")
|
|
174
|
+
for arr in ["types", "extensions", "exclude", "size"]:
|
|
175
|
+
if arr in arguments and not (
|
|
176
|
+
isinstance(arguments[arr], list)
|
|
177
|
+
and all(isinstance(x, str) for x in arguments[arr])
|
|
178
|
+
):
|
|
179
|
+
raise ValueError(f"{arr} must be an array of strings")
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
@handle_mcp_errors("list_files")
|
|
183
|
+
async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
184
|
+
# Check if fd command is available
|
|
185
|
+
if not fd_rg_utils.check_external_command("fd"):
|
|
186
|
+
return {
|
|
187
|
+
"success": False,
|
|
188
|
+
"error": "fd command not found. Please install fd (https://github.com/sharkdp/fd) to use this tool.",
|
|
189
|
+
"count": 0,
|
|
190
|
+
"results": [],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
self.validate_arguments(arguments)
|
|
194
|
+
roots = self._validate_roots(arguments["roots"]) # normalized absolutes
|
|
195
|
+
|
|
196
|
+
limit = fd_rg_utils.clamp_int(
|
|
197
|
+
arguments.get("limit"),
|
|
198
|
+
fd_rg_utils.DEFAULT_RESULTS_LIMIT,
|
|
199
|
+
fd_rg_utils.MAX_RESULTS_HARD_CAP,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Smart .gitignore detection
|
|
203
|
+
no_ignore = bool(arguments.get("no_ignore", False))
|
|
204
|
+
if not no_ignore:
|
|
205
|
+
# Auto-detect if we should use --no-ignore
|
|
206
|
+
detector = get_default_detector()
|
|
207
|
+
original_roots = arguments.get("roots", [])
|
|
208
|
+
should_ignore = detector.should_use_no_ignore(
|
|
209
|
+
original_roots, self.project_root
|
|
210
|
+
)
|
|
211
|
+
if should_ignore:
|
|
212
|
+
no_ignore = True
|
|
213
|
+
# Log the auto-detection for debugging
|
|
214
|
+
detection_info = detector.get_detection_info(
|
|
215
|
+
original_roots, self.project_root
|
|
216
|
+
)
|
|
217
|
+
logger.info(
|
|
218
|
+
f"Auto-enabled --no-ignore due to .gitignore interference: {detection_info['reason']}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
cmd = fd_rg_utils.build_fd_command(
|
|
222
|
+
pattern=arguments.get("pattern"),
|
|
223
|
+
glob=bool(arguments.get("glob", False)),
|
|
224
|
+
types=arguments.get("types"),
|
|
225
|
+
extensions=arguments.get("extensions"),
|
|
226
|
+
exclude=arguments.get("exclude"),
|
|
227
|
+
depth=arguments.get("depth"),
|
|
228
|
+
follow_symlinks=bool(arguments.get("follow_symlinks", False)),
|
|
229
|
+
hidden=bool(arguments.get("hidden", False)),
|
|
230
|
+
no_ignore=no_ignore, # Use the potentially auto-detected value
|
|
231
|
+
size=arguments.get("size"),
|
|
232
|
+
changed_within=arguments.get("changed_within"),
|
|
233
|
+
changed_before=arguments.get("changed_before"),
|
|
234
|
+
full_path_match=bool(arguments.get("full_path_match", False)),
|
|
235
|
+
absolute=True, # unify output to absolute paths
|
|
236
|
+
limit=limit,
|
|
237
|
+
roots=roots,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Use fd default path format (one per line). We'll determine is_dir and ext via Path
|
|
241
|
+
started = time.time()
|
|
242
|
+
rc, out, err = await fd_rg_utils.run_command_capture(cmd)
|
|
243
|
+
elapsed_ms = int((time.time() - started) * 1000)
|
|
244
|
+
|
|
245
|
+
if rc != 0:
|
|
246
|
+
message = err.decode("utf-8", errors="replace").strip() or "fd failed"
|
|
247
|
+
return {"success": False, "error": message, "returncode": rc}
|
|
248
|
+
|
|
249
|
+
lines = [
|
|
250
|
+
line.strip()
|
|
251
|
+
for line in out.decode("utf-8", errors="replace").splitlines()
|
|
252
|
+
if line.strip()
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Check if count_only mode is requested
|
|
256
|
+
if arguments.get("count_only", False):
|
|
257
|
+
total_count = len(lines)
|
|
258
|
+
# Apply hard cap for counting as well
|
|
259
|
+
if total_count > fd_rg_utils.MAX_RESULTS_HARD_CAP:
|
|
260
|
+
total_count = fd_rg_utils.MAX_RESULTS_HARD_CAP
|
|
261
|
+
truncated = True
|
|
262
|
+
else:
|
|
263
|
+
truncated = False
|
|
264
|
+
|
|
265
|
+
result = {
|
|
266
|
+
"success": True,
|
|
267
|
+
"count_only": True,
|
|
268
|
+
"total_count": total_count,
|
|
269
|
+
"truncated": truncated,
|
|
270
|
+
"elapsed_ms": elapsed_ms,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Handle file output for count_only mode
|
|
274
|
+
output_file = arguments.get("output_file")
|
|
275
|
+
suppress_output = arguments.get("suppress_output", False)
|
|
276
|
+
|
|
277
|
+
if output_file:
|
|
278
|
+
file_manager = FileOutputManager(self.project_root)
|
|
279
|
+
file_content = {
|
|
280
|
+
"count_only": True,
|
|
281
|
+
"total_count": total_count,
|
|
282
|
+
"truncated": truncated,
|
|
283
|
+
"elapsed_ms": elapsed_ms,
|
|
284
|
+
"query_info": {
|
|
285
|
+
"roots": arguments.get("roots", []),
|
|
286
|
+
"pattern": arguments.get("pattern"),
|
|
287
|
+
"glob": arguments.get("glob", False),
|
|
288
|
+
"types": arguments.get("types"),
|
|
289
|
+
"extensions": arguments.get("extensions"),
|
|
290
|
+
"exclude": arguments.get("exclude"),
|
|
291
|
+
"limit": limit,
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
import json
|
|
297
|
+
|
|
298
|
+
json_content = json.dumps(
|
|
299
|
+
file_content, indent=2, ensure_ascii=False
|
|
300
|
+
)
|
|
301
|
+
saved_path = file_manager.save_to_file(
|
|
302
|
+
content=json_content, base_name=output_file
|
|
303
|
+
)
|
|
304
|
+
result["output_file"] = saved_path # type: ignore[assignment]
|
|
305
|
+
|
|
306
|
+
if suppress_output:
|
|
307
|
+
# Return minimal response to save tokens
|
|
308
|
+
return {
|
|
309
|
+
"success": True,
|
|
310
|
+
"count_only": True,
|
|
311
|
+
"total_count": total_count,
|
|
312
|
+
"output_file": saved_path,
|
|
313
|
+
"message": f"Count results saved to {saved_path}",
|
|
314
|
+
}
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.warning(f"Failed to save output file: {e}")
|
|
317
|
+
result["output_file_error"] = str(e) # type: ignore[assignment]
|
|
318
|
+
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
# Truncate defensively even if fd didn't
|
|
322
|
+
truncated = False
|
|
323
|
+
if len(lines) > fd_rg_utils.MAX_RESULTS_HARD_CAP:
|
|
324
|
+
lines = lines[: fd_rg_utils.MAX_RESULTS_HARD_CAP]
|
|
325
|
+
truncated = True
|
|
326
|
+
|
|
327
|
+
results: list[dict[str, Any]] = []
|
|
328
|
+
for p in lines:
|
|
329
|
+
try:
|
|
330
|
+
path_obj = Path(p)
|
|
331
|
+
is_dir = path_obj.is_dir()
|
|
332
|
+
ext = path_obj.suffix[1:] if path_obj.suffix else None
|
|
333
|
+
size_bytes = None
|
|
334
|
+
mtime = None
|
|
335
|
+
try:
|
|
336
|
+
if not is_dir and path_obj.exists():
|
|
337
|
+
size_bytes = path_obj.stat().st_size
|
|
338
|
+
mtime = int(path_obj.stat().st_mtime)
|
|
339
|
+
except (OSError, ValueError): # nosec B110
|
|
340
|
+
pass
|
|
341
|
+
results.append(
|
|
342
|
+
{
|
|
343
|
+
"path": str(path_obj.resolve()),
|
|
344
|
+
"is_dir": is_dir,
|
|
345
|
+
"size_bytes": size_bytes,
|
|
346
|
+
"mtime": mtime,
|
|
347
|
+
"ext": ext,
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
except (OSError, ValueError): # nosec B112
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
final_result: dict[str, Any] = {
|
|
354
|
+
"success": True,
|
|
355
|
+
"count": len(results),
|
|
356
|
+
"truncated": truncated,
|
|
357
|
+
"elapsed_ms": elapsed_ms,
|
|
358
|
+
"results": results,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Handle file output for detailed results
|
|
362
|
+
output_file = arguments.get("output_file")
|
|
363
|
+
suppress_output = arguments.get("suppress_output", False)
|
|
364
|
+
|
|
365
|
+
if output_file:
|
|
366
|
+
file_manager = FileOutputManager(self.project_root)
|
|
367
|
+
file_content = {
|
|
368
|
+
"count": len(results),
|
|
369
|
+
"truncated": truncated,
|
|
370
|
+
"elapsed_ms": elapsed_ms,
|
|
371
|
+
"results": results,
|
|
372
|
+
"query_info": {
|
|
373
|
+
"roots": arguments.get("roots", []),
|
|
374
|
+
"pattern": arguments.get("pattern"),
|
|
375
|
+
"glob": arguments.get("glob", False),
|
|
376
|
+
"types": arguments.get("types"),
|
|
377
|
+
"extensions": arguments.get("extensions"),
|
|
378
|
+
"exclude": arguments.get("exclude"),
|
|
379
|
+
"depth": arguments.get("depth"),
|
|
380
|
+
"follow_symlinks": arguments.get("follow_symlinks", False),
|
|
381
|
+
"hidden": arguments.get("hidden", False),
|
|
382
|
+
"no_ignore": no_ignore,
|
|
383
|
+
"size": arguments.get("size"),
|
|
384
|
+
"changed_within": arguments.get("changed_within"),
|
|
385
|
+
"changed_before": arguments.get("changed_before"),
|
|
386
|
+
"full_path_match": arguments.get("full_path_match", False),
|
|
387
|
+
"absolute": arguments.get("absolute", True),
|
|
388
|
+
"limit": limit,
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
import json
|
|
394
|
+
|
|
395
|
+
json_content = json.dumps(file_content, indent=2, ensure_ascii=False)
|
|
396
|
+
saved_path = file_manager.save_to_file(
|
|
397
|
+
content=json_content, base_name=output_file
|
|
398
|
+
)
|
|
399
|
+
final_result["output_file"] = saved_path
|
|
400
|
+
|
|
401
|
+
if suppress_output:
|
|
402
|
+
# Return minimal response to save tokens
|
|
403
|
+
return {
|
|
404
|
+
"success": True,
|
|
405
|
+
"count": len(results),
|
|
406
|
+
"output_file": saved_path,
|
|
407
|
+
"message": f"File list results saved to {saved_path}",
|
|
408
|
+
}
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.warning(f"Failed to save output file: {e}")
|
|
411
|
+
final_result["output_file_error"] = str(e)
|
|
412
|
+
|
|
413
|
+
return final_result
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Output format parameter validation for search_content tool.
|
|
4
|
+
|
|
5
|
+
Ensures mutual exclusion of output format parameters to prevent conflicts
|
|
6
|
+
and provides multilingual error messages with token efficiency guidance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import locale
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OutputFormatValidator:
|
|
15
|
+
"""Validator for output format parameters mutual exclusion."""
|
|
16
|
+
|
|
17
|
+
# Output format parameters that are mutually exclusive
|
|
18
|
+
OUTPUT_FORMAT_PARAMS = {
|
|
19
|
+
"total_only",
|
|
20
|
+
"count_only_matches",
|
|
21
|
+
"summary_only",
|
|
22
|
+
"group_by_file",
|
|
23
|
+
"suppress_output",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Token efficiency guidance for error messages
|
|
27
|
+
FORMAT_EFFICIENCY_GUIDE = {
|
|
28
|
+
"total_only": "~10 tokens (most efficient for count queries)",
|
|
29
|
+
"count_only_matches": "~50-200 tokens (file distribution analysis)",
|
|
30
|
+
"summary_only": "~500-2000 tokens (initial investigation)",
|
|
31
|
+
"group_by_file": "~2000-10000 tokens (context-aware review)",
|
|
32
|
+
"suppress_output": "0 tokens (cache only, no output)",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def _detect_language(self) -> str:
|
|
36
|
+
"""Detect preferred language from environment."""
|
|
37
|
+
# Check environment variables for language preference
|
|
38
|
+
lang = os.environ.get("LANG", "")
|
|
39
|
+
if lang.startswith("ja"):
|
|
40
|
+
return "ja"
|
|
41
|
+
|
|
42
|
+
# Check locale
|
|
43
|
+
try:
|
|
44
|
+
current_locale = locale.getlocale()[0]
|
|
45
|
+
if current_locale and current_locale.startswith("ja"):
|
|
46
|
+
return "ja"
|
|
47
|
+
except Exception:
|
|
48
|
+
pass # nosec
|
|
49
|
+
|
|
50
|
+
# Default to English
|
|
51
|
+
return "en"
|
|
52
|
+
|
|
53
|
+
def _get_error_message(self, specified_formats: list[str]) -> str:
|
|
54
|
+
"""Generate localized error message with usage examples."""
|
|
55
|
+
lang = self._detect_language()
|
|
56
|
+
format_list = ", ".join(specified_formats)
|
|
57
|
+
|
|
58
|
+
if lang == "ja":
|
|
59
|
+
# Japanese error message
|
|
60
|
+
base_message = (
|
|
61
|
+
f"⚠️ 出力形式パラメータエラー: 相互排他的なパラメータが同時に指定されています: {format_list}\n\n"
|
|
62
|
+
f"🔒 相互排他的パラメータ: {', '.join(self.OUTPUT_FORMAT_PARAMS)}\n\n"
|
|
63
|
+
f"💡 トークン効率ガイド:\n"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for param, desc in self.FORMAT_EFFICIENCY_GUIDE.items():
|
|
67
|
+
base_message += f" • {param}: {desc}\n"
|
|
68
|
+
|
|
69
|
+
base_message += (
|
|
70
|
+
"\n📋 推奨使用パターン:\n"
|
|
71
|
+
" • 件数確認: total_only=true\n"
|
|
72
|
+
" • ファイル分布: count_only_matches=true\n"
|
|
73
|
+
" • 初期調査: summary_only=true\n"
|
|
74
|
+
" • 詳細レビュー: group_by_file=true\n"
|
|
75
|
+
" • キャッシュのみ: suppress_output=true\n\n"
|
|
76
|
+
'❌ 間違った例: {"total_only": true, "summary_only": true}\n'
|
|
77
|
+
'✅ 正しい例: {"total_only": true}'
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# English error message
|
|
81
|
+
base_message = (
|
|
82
|
+
f"⚠️ Output Format Parameter Error: Multiple mutually exclusive formats specified: {format_list}\n\n"
|
|
83
|
+
f"🔒 Mutually Exclusive Parameters: {', '.join(self.OUTPUT_FORMAT_PARAMS)}\n\n"
|
|
84
|
+
f"💡 Token Efficiency Guide:\n"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for param, desc in self.FORMAT_EFFICIENCY_GUIDE.items():
|
|
88
|
+
base_message += f" • {param}: {desc}\n"
|
|
89
|
+
|
|
90
|
+
base_message += (
|
|
91
|
+
"\n📋 Recommended Usage Patterns:\n"
|
|
92
|
+
" • Count validation: total_only=true\n"
|
|
93
|
+
" • File distribution: count_only_matches=true\n"
|
|
94
|
+
" • Initial investigation: summary_only=true\n"
|
|
95
|
+
" • Detailed review: group_by_file=true\n"
|
|
96
|
+
" • Cache only: suppress_output=true\n\n"
|
|
97
|
+
'❌ Incorrect: {"total_only": true, "summary_only": true}\n'
|
|
98
|
+
'✅ Correct: {"total_only": true}'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return base_message
|
|
102
|
+
|
|
103
|
+
def validate_output_format_exclusion(self, arguments: dict[str, Any]) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Validate that only one output format parameter is specified.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
arguments: Tool arguments dictionary
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If multiple output format parameters are specified
|
|
112
|
+
"""
|
|
113
|
+
specified_formats = []
|
|
114
|
+
|
|
115
|
+
for param in self.OUTPUT_FORMAT_PARAMS:
|
|
116
|
+
if arguments.get(param, False):
|
|
117
|
+
specified_formats.append(param)
|
|
118
|
+
|
|
119
|
+
if len(specified_formats) > 1:
|
|
120
|
+
error_message = self._get_error_message(specified_formats)
|
|
121
|
+
raise ValueError(error_message)
|
|
122
|
+
|
|
123
|
+
def get_active_format(self, arguments: dict[str, Any]) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Get the active output format from arguments.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
arguments: Tool arguments dictionary
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Active format name or "normal" if none specified
|
|
132
|
+
"""
|
|
133
|
+
for param in self.OUTPUT_FORMAT_PARAMS:
|
|
134
|
+
if arguments.get(param, False):
|
|
135
|
+
return param
|
|
136
|
+
return "normal"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Global validator instance
|
|
140
|
+
_default_validator: OutputFormatValidator | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_default_validator() -> OutputFormatValidator:
|
|
144
|
+
"""Get the default output format validator instance."""
|
|
145
|
+
global _default_validator
|
|
146
|
+
if _default_validator is None:
|
|
147
|
+
_default_validator = OutputFormatValidator()
|
|
148
|
+
return _default_validator
|