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,443 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Query Tool for MCP
4
+
5
+ MCP tool providing tree-sitter query functionality using unified QueryService.
6
+ Supports both predefined query keys and custom query strings.
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ...core.query_service import QueryService
14
+ from ...language_detector import detect_language_from_file
15
+ from ..utils.file_output_manager import FileOutputManager
16
+ from .base_tool import BaseMCPTool
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class QueryTool(BaseMCPTool):
22
+ """MCP query tool providing tree-sitter query functionality"""
23
+
24
+ def __init__(self, project_root: str | None = None) -> None:
25
+ """Initialize query tool"""
26
+ super().__init__(project_root)
27
+ self.query_service = QueryService(project_root)
28
+ self.file_output_manager = FileOutputManager.get_managed_instance(project_root)
29
+
30
+ def set_project_path(self, project_path: str) -> None:
31
+ """
32
+ Update the project path for all components.
33
+
34
+ Args:
35
+ project_path: New project root directory
36
+ """
37
+ super().set_project_path(project_path)
38
+ self.query_service = QueryService(project_path)
39
+ self.file_output_manager = FileOutputManager.get_managed_instance(project_path)
40
+ logger.info(f"QueryTool project path updated to: {project_path}")
41
+
42
+ def get_tool_definition(self) -> dict[str, Any]:
43
+ """
44
+ Get MCP tool definition
45
+
46
+ Returns:
47
+ Tool definition dictionary
48
+ """
49
+ return {
50
+ "name": "query_code",
51
+ "description": "Execute tree-sitter queries on code files to extract specific code elements with filtering, multiple output formats, and optional file saving with token optimization",
52
+ "inputSchema": {
53
+ "type": "object",
54
+ "properties": {
55
+ "file_path": {
56
+ "type": "string",
57
+ "description": "Path to the code file to query (relative to project root or absolute path)",
58
+ },
59
+ "language": {
60
+ "type": "string",
61
+ "description": "Programming language (optional, auto-detected if not provided)",
62
+ },
63
+ "query_key": {
64
+ "type": "string",
65
+ "description": "Predefined query key (e.g., 'methods', 'class', 'functions')",
66
+ },
67
+ "query_string": {
68
+ "type": "string",
69
+ "description": "Custom tree-sitter query string (e.g., '(method_declaration) @method')",
70
+ },
71
+ "filter": {
72
+ "type": "string",
73
+ "description": "Filter expression to refine results (e.g., 'name=main', 'name=~get*,public=true')",
74
+ },
75
+ "output_format": {
76
+ "type": "string",
77
+ "enum": ["json", "summary"],
78
+ "default": "json",
79
+ "description": "Output format",
80
+ },
81
+ "output_file": {
82
+ "type": "string",
83
+ "description": "Optional filename to save output to file (extension auto-detected based on content)",
84
+ },
85
+ "suppress_output": {
86
+ "type": "boolean",
87
+ "description": "When true and output_file is specified, suppress detailed output in response to save tokens",
88
+ "default": False,
89
+ },
90
+ },
91
+ "required": ["file_path"],
92
+ },
93
+ }
94
+
95
+ async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
96
+ """
97
+ Execute query tool
98
+
99
+ Args:
100
+ arguments: Tool arguments
101
+
102
+ Returns:
103
+ Query results
104
+ """
105
+ try:
106
+ # Validate input parameters - check for empty arguments first
107
+ if not arguments:
108
+ from ..utils.error_handler import AnalysisError
109
+
110
+ raise AnalysisError("file_path is required", operation="query_code")
111
+
112
+ file_path = arguments.get("file_path")
113
+ if not file_path:
114
+ from ..utils.error_handler import AnalysisError
115
+
116
+ raise AnalysisError("file_path is required", operation="query_code")
117
+
118
+ # Check that either query_key or query_string is provided early
119
+ query_key = arguments.get("query_key")
120
+ query_string = arguments.get("query_string")
121
+
122
+ if not query_key and not query_string:
123
+ from ..utils.error_handler import AnalysisError
124
+
125
+ raise AnalysisError(
126
+ "Either query_key or query_string must be provided",
127
+ operation="query_code",
128
+ )
129
+
130
+ # Security validation BEFORE path resolution to catch symlinks
131
+ is_valid, error_msg = self.security_validator.validate_file_path(file_path)
132
+ if not is_valid:
133
+ return {
134
+ "success": False,
135
+ "error": f"Invalid or unsafe file path: {error_msg or file_path}",
136
+ }
137
+
138
+ # Resolve file path to absolute path
139
+ resolved_file_path = self.path_resolver.resolve(file_path)
140
+ logger.info(
141
+ f"Querying file: {file_path} (resolved to: {resolved_file_path})"
142
+ )
143
+
144
+ # Additional security validation on resolved path
145
+ is_valid, error_msg = self.security_validator.validate_file_path(
146
+ resolved_file_path
147
+ )
148
+ if not is_valid:
149
+ return {
150
+ "success": False,
151
+ "error": f"Invalid or unsafe resolved path: {error_msg or resolved_file_path}",
152
+ }
153
+
154
+ # Get query parameters (already validated above)
155
+ filter_expression = arguments.get("filter")
156
+ output_format = arguments.get("output_format", "json")
157
+ output_file = arguments.get("output_file")
158
+ suppress_output = arguments.get("suppress_output", False)
159
+
160
+ if query_key and query_string:
161
+ return {
162
+ "success": False,
163
+ "error": "Cannot provide both query_key and query_string",
164
+ }
165
+
166
+ # Detect language
167
+ language = arguments.get("language")
168
+ if not language:
169
+ language = detect_language_from_file(resolved_file_path)
170
+ if not language:
171
+ return {
172
+ "success": False,
173
+ "error": f"Could not detect language for file: {file_path}",
174
+ }
175
+
176
+ # Execute query
177
+ results = await self.query_service.execute_query(
178
+ resolved_file_path, language, query_key, query_string, filter_expression
179
+ )
180
+
181
+ if not results:
182
+ return {
183
+ "success": True,
184
+ "message": "No results found matching the query",
185
+ "results": [],
186
+ "count": 0,
187
+ }
188
+
189
+ # Format output
190
+ if output_format == "summary":
191
+ formatted_result = self._format_summary(
192
+ results, query_key or "custom", language
193
+ )
194
+ else:
195
+ formatted_result = {
196
+ "success": True,
197
+ "results": results,
198
+ "count": len(results),
199
+ "file_path": file_path,
200
+ "language": language,
201
+ "query": query_key or query_string,
202
+ }
203
+
204
+ # Handle file output if requested
205
+ if output_file:
206
+ try:
207
+ import json
208
+
209
+ # Generate base name from original file path if not provided
210
+ if not output_file or output_file.strip() == "":
211
+ base_name = (
212
+ f"{Path(file_path).stem}_query_{query_key or 'custom'}"
213
+ )
214
+ else:
215
+ base_name = output_file
216
+
217
+ # Convert result to JSON string for file output
218
+ json_content = json.dumps(
219
+ formatted_result, indent=2, ensure_ascii=False
220
+ )
221
+
222
+ # Save to file with automatic extension detection
223
+ saved_file_path = self.file_output_manager.save_to_file(
224
+ content=json_content, base_name=base_name
225
+ )
226
+
227
+ # Add file output info to result
228
+ formatted_result["output_file_path"] = saved_file_path
229
+ formatted_result["file_saved"] = True
230
+
231
+ logger.info(f"Query output saved to: {saved_file_path}")
232
+
233
+ except Exception as e:
234
+ logger.error(f"Failed to save output to file: {e}")
235
+ formatted_result["file_save_error"] = str(e)
236
+ formatted_result["file_saved"] = False
237
+
238
+ # Apply suppress_output logic
239
+ if suppress_output and output_file:
240
+ # Create minimal result when output is suppressed
241
+ minimal_result = {
242
+ "success": formatted_result.get("success", True),
243
+ "count": formatted_result.get("count", len(results)),
244
+ "file_path": file_path,
245
+ "language": language,
246
+ "query": query_key or query_string,
247
+ }
248
+
249
+ # Include file output info if present
250
+ if "output_file_path" in formatted_result:
251
+ minimal_result["output_file_path"] = formatted_result[
252
+ "output_file_path"
253
+ ]
254
+ minimal_result["file_saved"] = formatted_result["file_saved"]
255
+ if "file_save_error" in formatted_result:
256
+ minimal_result["file_save_error"] = formatted_result[
257
+ "file_save_error"
258
+ ]
259
+ minimal_result["file_saved"] = formatted_result["file_saved"]
260
+
261
+ return minimal_result
262
+ else:
263
+ return formatted_result
264
+
265
+ except Exception as e:
266
+ from ..utils.error_handler import AnalysisError
267
+
268
+ # Re-raise AnalysisError to maintain proper error handling
269
+ if isinstance(e, AnalysisError):
270
+ raise
271
+
272
+ logger.error(f"Query execution failed: {e}")
273
+ return {
274
+ "success": False,
275
+ "error": str(e),
276
+ "file_path": arguments.get("file_path", "unknown"),
277
+ "language": arguments.get("language", "unknown"),
278
+ }
279
+
280
+ def _format_summary(
281
+ self, results: list[dict[str, Any]], query_type: str, language: str
282
+ ) -> dict[str, Any]:
283
+ """
284
+ Format summary output
285
+
286
+ Args:
287
+ results: Query results
288
+ query_type: Query type
289
+ language: Programming language
290
+
291
+ Returns:
292
+ Summary formatted results
293
+ """
294
+ # Group by capture name
295
+ by_capture: dict[str, list[dict[str, Any]]] = {}
296
+ for result in results:
297
+ capture_name = result["capture_name"]
298
+ if capture_name not in by_capture:
299
+ by_capture[capture_name] = []
300
+ by_capture[capture_name].append(result)
301
+
302
+ # Create summary
303
+ summary: dict[str, Any] = {
304
+ "success": True,
305
+ "query_type": query_type,
306
+ "language": language,
307
+ "total_count": len(results),
308
+ "captures": {},
309
+ }
310
+
311
+ for capture_name, items in by_capture.items():
312
+ summary["captures"][capture_name] = {
313
+ "count": len(items),
314
+ "items": [
315
+ {
316
+ "name": self._extract_name_from_content(item["content"]),
317
+ "line_range": f"{item['start_line']}-{item['end_line']}",
318
+ "node_type": item["node_type"],
319
+ }
320
+ for item in items
321
+ ],
322
+ }
323
+
324
+ return summary
325
+
326
+ def _extract_name_from_content(self, content: str) -> str:
327
+ """
328
+ Extract name from content (simple heuristic method)
329
+
330
+ Args:
331
+ content: Code content
332
+
333
+ Returns:
334
+ Extracted name
335
+ """
336
+ # Simple name extraction logic, can be improved as needed
337
+ lines = content.strip().split("\n")
338
+ if lines:
339
+ first_line = lines[0].strip()
340
+ # Extract method names, class names, etc.
341
+ import re
342
+
343
+ # Match common declaration patterns
344
+ patterns = [
345
+ # Markdown headers
346
+ r"^#{1,6}\s+(.+)$", # Markdown headers (# Title, ## Subtitle, etc.)
347
+ # Programming language patterns
348
+ r"(?:public|private|protected)?\s*(?:static)?\s*(?:class|interface)\s+(\w+)", # class/interface
349
+ r"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", # method
350
+ r"(\w+)\s*\(", # simple function call
351
+ ]
352
+
353
+ for pattern in patterns:
354
+ match = re.search(pattern, first_line)
355
+ if match:
356
+ return match.group(1).strip()
357
+
358
+ return "unnamed"
359
+
360
+ def get_available_queries(self, language: str) -> list[str]:
361
+ """
362
+ Get available query keys
363
+
364
+ Args:
365
+ language: Programming language
366
+
367
+ Returns:
368
+ List of available query keys
369
+ """
370
+ return self.query_service.get_available_queries(language)
371
+
372
+ def validate_arguments(self, arguments: dict[str, Any]) -> bool:
373
+ """
374
+ Validate tool arguments.
375
+
376
+ Args:
377
+ arguments: Arguments to validate
378
+
379
+ Returns:
380
+ True if arguments are valid
381
+
382
+ Raises:
383
+ ValueError: If arguments are invalid
384
+ """
385
+ # Check required fields
386
+ if "file_path" not in arguments:
387
+ raise ValueError("file_path is required")
388
+
389
+ # Validate file_path
390
+ file_path = arguments["file_path"]
391
+ if not isinstance(file_path, str):
392
+ raise ValueError("file_path must be a string")
393
+ if not file_path.strip():
394
+ raise ValueError("file_path cannot be empty")
395
+
396
+ # Check that either query_key or query_string is provided
397
+ query_key = arguments.get("query_key")
398
+ query_string = arguments.get("query_string")
399
+
400
+ if not query_key and not query_string:
401
+ raise ValueError("Either query_key or query_string must be provided")
402
+
403
+ # Validate query_key if provided
404
+ if query_key and not isinstance(query_key, str):
405
+ raise ValueError("query_key must be a string")
406
+
407
+ # Validate query_string if provided
408
+ if query_string and not isinstance(query_string, str):
409
+ raise ValueError("query_string must be a string")
410
+
411
+ # Validate optional fields
412
+ if "language" in arguments:
413
+ language = arguments["language"]
414
+ if not isinstance(language, str):
415
+ raise ValueError("language must be a string")
416
+
417
+ if "filter" in arguments:
418
+ filter_expr = arguments["filter"]
419
+ if not isinstance(filter_expr, str):
420
+ raise ValueError("filter must be a string")
421
+
422
+ if "output_format" in arguments:
423
+ output_format = arguments["output_format"]
424
+ if not isinstance(output_format, str):
425
+ raise ValueError("output_format must be a string")
426
+ if output_format not in ["json", "summary"]:
427
+ raise ValueError("output_format must be one of: json, summary")
428
+
429
+ # Validate output_file if provided
430
+ if "output_file" in arguments:
431
+ output_file = arguments["output_file"]
432
+ if not isinstance(output_file, str):
433
+ raise ValueError("output_file must be a string")
434
+ if not output_file.strip():
435
+ raise ValueError("output_file cannot be empty")
436
+
437
+ # Validate suppress_output if provided
438
+ if "suppress_output" in arguments:
439
+ suppress_output = arguments["suppress_output"]
440
+ if not isinstance(suppress_output, bool):
441
+ raise ValueError("suppress_output must be a boolean")
442
+
443
+ return True