tree-sitter-analyzer 1.7.0__py3-none-any.whl → 1.7.2__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.

@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import Any
14
14
 
15
15
  from ..utils.error_handler import handle_mcp_errors
16
+ from ..utils.file_output_manager import FileOutputManager
16
17
  from ..utils.gitignore_detector import get_default_detector
17
18
  from ..utils.search_cache import get_default_cache
18
19
  from . import fd_rg_utils
@@ -36,6 +37,7 @@ class SearchContentTool(BaseMCPTool):
36
37
  """
37
38
  super().__init__(project_root)
38
39
  self.cache = get_default_cache() if enable_cache else None
40
+ self.file_output_manager = FileOutputManager(project_root)
39
41
 
40
42
  def get_tool_definition(self) -> dict[str, Any]:
41
43
  return {
@@ -153,6 +155,15 @@ class SearchContentTool(BaseMCPTool):
153
155
  "default": False,
154
156
  "description": "Return only the total match count as a number. Most token-efficient option for count queries. Takes priority over all other formats",
155
157
  },
158
+ "output_file": {
159
+ "type": "string",
160
+ "description": "Optional filename to save output to file (extension auto-detected based on content)",
161
+ },
162
+ "suppress_output": {
163
+ "type": "boolean",
164
+ "description": "When true and output_file is specified, suppress detailed output in response to save tokens",
165
+ "default": False,
166
+ },
156
167
  },
157
168
  "required": ["query"],
158
169
  "anyOf": [
@@ -312,12 +323,18 @@ class SearchContentTool(BaseMCPTool):
312
323
  cached_result["cache_hit"] = True
313
324
  return cached_result
314
325
 
315
- # Clamp counts to safety limits
316
- max_count = fd_rg_utils.clamp_int(
317
- arguments.get("max_count"),
318
- fd_rg_utils.DEFAULT_RESULTS_LIMIT,
319
- fd_rg_utils.DEFAULT_RESULTS_LIMIT,
320
- )
326
+ # Handle max_count parameter properly
327
+ # If user specifies max_count, use it directly (with reasonable upper limit)
328
+ # If not specified, use None to let ripgrep return all matches (subject to hard cap later)
329
+ max_count = arguments.get("max_count")
330
+ if max_count is not None:
331
+ # Clamp user-specified max_count to reasonable limits
332
+ # Use 1 as minimum default, but respect user's small values
333
+ max_count = fd_rg_utils.clamp_int(
334
+ max_count,
335
+ 1, # Minimum default value
336
+ fd_rg_utils.DEFAULT_RESULTS_LIMIT, # Upper limit for safety
337
+ )
321
338
  timeout_ms = arguments.get("timeout_ms")
322
339
 
323
340
  # Note: --files-from is not supported in this ripgrep version
@@ -461,9 +478,18 @@ class SearchContentTool(BaseMCPTool):
461
478
 
462
479
  # Handle normal mode
463
480
  matches = fd_rg_utils.parse_rg_json_lines_to_matches(out)
464
- truncated = len(matches) >= fd_rg_utils.MAX_RESULTS_HARD_CAP
465
- if truncated:
466
- matches = matches[: fd_rg_utils.MAX_RESULTS_HARD_CAP]
481
+
482
+ # Apply user-specified max_count limit if provided
483
+ # Note: ripgrep's -m option limits matches per file, not total matches
484
+ # So we need to apply the total limit here in post-processing
485
+ user_max_count = arguments.get("max_count")
486
+ if user_max_count is not None and len(matches) > user_max_count:
487
+ matches = matches[:user_max_count]
488
+ truncated = True
489
+ else:
490
+ truncated = len(matches) >= fd_rg_utils.MAX_RESULTS_HARD_CAP
491
+ if truncated:
492
+ matches = matches[: fd_rg_utils.MAX_RESULTS_HARD_CAP]
467
493
 
468
494
  # Apply path optimization if requested
469
495
  optimize_paths = arguments.get("optimize_paths", False)
@@ -475,6 +501,54 @@ class SearchContentTool(BaseMCPTool):
475
501
  if group_by_file and matches:
476
502
  result = fd_rg_utils.group_matches_by_file(matches)
477
503
 
504
+ # Handle output suppression and file output for grouped results
505
+ output_file = arguments.get("output_file")
506
+ suppress_output = arguments.get("suppress_output", False)
507
+
508
+ # Handle file output if requested
509
+ if output_file:
510
+ try:
511
+ # Save full result to file
512
+ import json
513
+ json_content = json.dumps(result, indent=2, ensure_ascii=False)
514
+ file_path = self.file_output_manager.save_to_file(
515
+ content=json_content,
516
+ base_name=output_file
517
+ )
518
+
519
+ # If suppress_output is True, return minimal response
520
+ if suppress_output:
521
+ minimal_result = {
522
+ "success": result.get("success", True),
523
+ "count": result.get("count", 0),
524
+ "output_file": output_file,
525
+ "file_saved": f"Results saved to {file_path}"
526
+ }
527
+ # Cache the full result, not the minimal one
528
+ if self.cache and cache_key:
529
+ self.cache.set(cache_key, result)
530
+ return minimal_result
531
+ else:
532
+ # Include file info in full response
533
+ result["output_file"] = output_file
534
+ result["file_saved"] = f"Results saved to {file_path}"
535
+ except Exception as e:
536
+ logger.error(f"Failed to save output to file: {e}")
537
+ result["file_save_error"] = str(e)
538
+ result["file_saved"] = False
539
+ elif suppress_output:
540
+ # If suppress_output is True but no output_file, remove detailed results
541
+ minimal_result = {
542
+ "success": result.get("success", True),
543
+ "count": result.get("count", 0),
544
+ "summary": result.get("summary", {}),
545
+ "meta": result.get("meta", {})
546
+ }
547
+ # Cache the full result, not the minimal one
548
+ if self.cache and cache_key:
549
+ self.cache.set(cache_key, result)
550
+ return minimal_result
551
+
478
552
  # Cache the result
479
553
  if self.cache and cache_key:
480
554
  self.cache.set(cache_key, result)
@@ -492,6 +566,54 @@ class SearchContentTool(BaseMCPTool):
492
566
  "summary": summary,
493
567
  }
494
568
 
569
+ # Handle output suppression and file output for summary results
570
+ output_file = arguments.get("output_file")
571
+ suppress_output = arguments.get("suppress_output", False)
572
+
573
+ # Handle file output if requested
574
+ if output_file:
575
+ try:
576
+ # Save full result to file
577
+ import json
578
+ json_content = json.dumps(result, indent=2, ensure_ascii=False)
579
+ file_path = self.file_output_manager.save_to_file(
580
+ content=json_content,
581
+ base_name=output_file
582
+ )
583
+
584
+ # If suppress_output is True, return minimal response
585
+ if suppress_output:
586
+ minimal_result = {
587
+ "success": result.get("success", True),
588
+ "count": result.get("count", 0),
589
+ "output_file": output_file,
590
+ "file_saved": f"Results saved to {file_path}"
591
+ }
592
+ # Cache the full result, not the minimal one
593
+ if self.cache and cache_key:
594
+ self.cache.set(cache_key, result)
595
+ return minimal_result
596
+ else:
597
+ # Include file info in full response
598
+ result["output_file"] = output_file
599
+ result["file_saved"] = f"Results saved to {file_path}"
600
+ except Exception as e:
601
+ logger.error(f"Failed to save output to file: {e}")
602
+ result["file_save_error"] = str(e)
603
+ result["file_saved"] = False
604
+ elif suppress_output:
605
+ # If suppress_output is True but no output_file, remove detailed results
606
+ minimal_result = {
607
+ "success": result.get("success", True),
608
+ "count": result.get("count", 0),
609
+ "summary": result.get("summary", {}),
610
+ "elapsed_ms": result.get("elapsed_ms", 0)
611
+ }
612
+ # Cache the full result, not the minimal one
613
+ if self.cache and cache_key:
614
+ self.cache.set(cache_key, result)
615
+ return minimal_result
616
+
495
617
  # Cache the result
496
618
  if self.cache and cache_key:
497
619
  self.cache.set(cache_key, result)
@@ -503,9 +625,87 @@ class SearchContentTool(BaseMCPTool):
503
625
  "count": len(matches),
504
626
  "truncated": truncated,
505
627
  "elapsed_ms": elapsed_ms,
506
- "results": matches,
507
628
  }
508
629
 
630
+ # Handle output suppression and file output
631
+ output_file = arguments.get("output_file")
632
+ suppress_output = arguments.get("suppress_output", False)
633
+
634
+ # Always add results to the base result for file saving
635
+ result["results"] = matches
636
+
637
+ # Handle file output if requested
638
+ if output_file:
639
+ try:
640
+ # Create detailed output for file
641
+ file_content = {
642
+ "success": True,
643
+ "count": len(matches),
644
+ "truncated": truncated,
645
+ "elapsed_ms": elapsed_ms,
646
+ "results": matches,
647
+ "summary": fd_rg_utils.summarize_search_results(matches),
648
+ "grouped_by_file": fd_rg_utils.group_matches_by_file(matches)["files"] if matches else []
649
+ }
650
+
651
+ # Convert to JSON for file output
652
+ import json
653
+ json_content = json.dumps(file_content, indent=2, ensure_ascii=False)
654
+
655
+ # Save to file
656
+ saved_file_path = self.file_output_manager.save_to_file(
657
+ content=json_content,
658
+ base_name=output_file
659
+ )
660
+
661
+ result["output_file_path"] = saved_file_path
662
+ result["file_saved"] = True
663
+
664
+ logger.info(f"Search results saved to: {saved_file_path}")
665
+
666
+ except Exception as e:
667
+ logger.error(f"Failed to save output to file: {e}")
668
+ result["file_save_error"] = str(e)
669
+ result["file_saved"] = False
670
+
671
+ # Handle file output and suppression
672
+ output_file = arguments.get("output_file")
673
+ suppress_output = arguments.get("suppress_output", False)
674
+
675
+ if output_file:
676
+ # Save full result to file
677
+ import json
678
+ json_content = json.dumps(result, indent=2, ensure_ascii=False)
679
+ file_path = self.file_output_manager.save_to_file(
680
+ content=json_content,
681
+ base_name=output_file
682
+ )
683
+
684
+ # If suppress_output is True, return minimal response
685
+ if suppress_output:
686
+ minimal_result = {
687
+ "success": result.get("success", True),
688
+ "count": result.get("count", 0),
689
+ "output_file": output_file,
690
+ "file_saved": f"Results saved to {file_path}"
691
+ }
692
+ # Cache the full result, not the minimal one
693
+ if self.cache and cache_key:
694
+ self.cache.set(cache_key, result)
695
+ return minimal_result
696
+ else:
697
+ # Include file info in full response
698
+ result["output_file"] = output_file
699
+ result["file_saved"] = f"Results saved to {file_path}"
700
+ elif suppress_output:
701
+ # If suppress_output is True but no output_file, remove results from response
702
+ result_copy = result.copy()
703
+ result_copy.pop("results", None)
704
+ # Cache the full result, not the minimal one
705
+ if self.cache and cache_key:
706
+ self.cache.set(cache_key, result)
707
+ return result_copy
708
+
509
709
  # Cache the result
510
710
  if self.cache and cache_key:
511
711
  self.cache.set(cache_key, result)