"""Shared utilities for file and shell tools.""" # Default truncation limits DEFAULT_MAX_LINES = 1000 DEFAULT_MAX_BYTES = 30 * 1024 # 30KB def truncate_output( text: str, max_lines: int = DEFAULT_MAX_LINES, max_bytes: int = DEFAULT_MAX_BYTES, keep: str = "head", ) -> tuple[str, bool, int, str]: """Smart truncation for large content. Args: text: Text content to truncate. max_lines: Maximum number of lines. max_bytes: Maximum size in bytes. keep: Which part to keep - "head" (first lines) or "tail" (last lines). Returns: (truncated_content, was_truncated, output_line_count, truncate_reason) """ if not text: return text, False, 0, "" lines = text.split("\n") total_lines = len(lines) # No truncation needed if total_lines <= max_lines and len(text.encode("utf-8")) <= max_bytes: return text, False, total_lines, "" # Apply line limit if total_lines > max_lines: if keep == "tail": lines = lines[-max_lines:] else: lines = lines[:max_lines] reason = "lines" else: reason = "" # Apply byte limit if len("\n".join(lines).encode("utf-8")) > max_bytes: if keep == "tail": while lines and len("\n".join(lines).encode("utf-8")) > max_bytes: lines.pop(0) else: truncated = [] current_bytes = 0 for line in lines: line_bytes = len(line.encode("utf-8")) + 1 if current_bytes + line_bytes > max_bytes: break truncated.append(line) current_bytes += line_bytes lines = truncated reason = "bytes" return "\n".join(lines), True, len(lines), reason def truncate_shell_output(text: str) -> str: """Truncate shell output to last N lines or M bytes, with truncation notice. Args: text: The output text to truncate. Returns: Truncated text with notice if truncated. """ if not text: return text try: total_lines = len(text.split("\n")) truncated, was_truncated, output_lines, reason = truncate_output(text, keep="tail") if not was_truncated: return text start_line = total_lines - output_lines + 1 if reason == "lines": notice = f"\n\n[Output truncated: showing lines {start_line}-{total_lines} of {total_lines} total]" else: notice = ( f"\n\n[Output truncated: showing lines {start_line}-{total_lines} of {total_lines} " f"({DEFAULT_MAX_BYTES // 1024}KB limit)]" ) return truncated + notice except Exception: return text def read_file_safe(file_path: str) -> str: """Read file with Unicode error handling. Args: file_path: Path to the file. Returns: File content as string. """ try: with open(file_path, "r", encoding="utf-8") as f: return f.read() except UnicodeDecodeError: with open(file_path, "r", encoding="utf-8", errors="ignore") as f: return f.read()