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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. 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