"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.GREP_MAX_LINE_LENGTH = exports.DEFAULT_MAX_LINES = exports.DEFAULT_MAX_BYTES = void 0;exports.formatSize = formatSize;exports.truncateHead = truncateHead;exports.truncateLine = truncateLine;exports.truncateTail = truncateTail; /** * Shared truncation utilities for tool outputs. * * Truncation is based on two independent limits - whichever is hit first wins: * - Line limit (default: 2000 lines) * - Byte limit (default: 50KB) * * Never returns partial lines (except bash tail truncation edge case). */ const DEFAULT_MAX_LINES = exports.DEFAULT_MAX_LINES = 2000; const DEFAULT_MAX_BYTES = exports.DEFAULT_MAX_BYTES = 50 * 1024; // 50KB const GREP_MAX_LINE_LENGTH = exports.GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line /** * Format bytes as human-readable size. */ function formatSize(bytes) { if (bytes < 1024) { return `${bytes}B`; } else if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)}KB`; } else { return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; } } /** * Truncate content from the head (keep first N lines/bytes). * Suitable for file reads where you want to see the beginning. * * Never returns partial lines. If first line exceeds byte limit, * returns empty content with firstLineExceedsLimit=true. */ function truncateHead(content, options = {}) { const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; const totalBytes = Buffer.byteLength(content, "utf-8"); const lines = content.split("\n"); const totalLines = lines.length; // Check if no truncation needed if (totalLines <= maxLines && totalBytes <= maxBytes) { return { content, truncated: false, truncatedBy: null, totalLines, totalBytes, outputLines: totalLines, outputBytes: totalBytes, lastLinePartial: false, firstLineExceedsLimit: false, maxLines, maxBytes }; } // Check if first line alone exceeds byte limit const firstLineBytes = Buffer.byteLength(lines[0], "utf-8"); if (firstLineBytes > maxBytes) { return { content: "", truncated: true, truncatedBy: "bytes", totalLines, totalBytes, outputLines: 0, outputBytes: 0, lastLinePartial: false, firstLineExceedsLimit: true, maxLines, maxBytes }; } // Collect complete lines that fit const outputLinesArr = []; let outputBytesCount = 0; let truncatedBy = "lines"; for (let i = 0; i < lines.length && i < maxLines; i++) { const line = lines[i]; const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline if (outputBytesCount + lineBytes > maxBytes) { truncatedBy = "bytes"; break; } outputLinesArr.push(line); outputBytesCount += lineBytes; } // If we exited due to line limit if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { truncatedBy = "lines"; } const outputContent = outputLinesArr.join("\n"); const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); return { content: outputContent, truncated: true, truncatedBy, totalLines, totalBytes, outputLines: outputLinesArr.length, outputBytes: finalOutputBytes, lastLinePartial: false, firstLineExceedsLimit: false, maxLines, maxBytes }; } /** * Truncate content from the tail (keep last N lines/bytes). * Suitable for bash output where you want to see the end (errors, final results). * * May return partial first line if the last line of original content exceeds byte limit. */ function truncateTail(content, options = {}) { const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; const totalBytes = Buffer.byteLength(content, "utf-8"); const lines = content.split("\n"); const totalLines = lines.length; // Check if no truncation needed if (totalLines <= maxLines && totalBytes <= maxBytes) { return { content, truncated: false, truncatedBy: null, totalLines, totalBytes, outputLines: totalLines, outputBytes: totalBytes, lastLinePartial: false, firstLineExceedsLimit: false, maxLines, maxBytes }; } // Work backwards from the end const outputLinesArr = []; let outputBytesCount = 0; let truncatedBy = "lines"; let lastLinePartial = false; for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) { const line = lines[i]; const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline if (outputBytesCount + lineBytes > maxBytes) { truncatedBy = "bytes"; // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes, // take the end of the line (partial) if (outputLinesArr.length === 0) { const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes); outputLinesArr.unshift(truncatedLine); outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8"); lastLinePartial = true; } break; } outputLinesArr.unshift(line); outputBytesCount += lineBytes; } // If we exited due to line limit if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { truncatedBy = "lines"; } const outputContent = outputLinesArr.join("\n"); const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); return { content: outputContent, truncated: true, truncatedBy, totalLines, totalBytes, outputLines: outputLinesArr.length, outputBytes: finalOutputBytes, lastLinePartial, firstLineExceedsLimit: false, maxLines, maxBytes }; } /** * Truncate a string to fit within a byte limit (from the end). * Handles multi-byte UTF-8 characters correctly. */ function truncateStringToBytesFromEnd(str, maxBytes) { const buf = Buffer.from(str, "utf-8"); if (buf.length <= maxBytes) { return str; } // Start from the end, skip maxBytes back let start = buf.length - maxBytes; // Find a valid UTF-8 boundary (start of a character) while (start < buf.length && (buf[start] & 0xc0) === 0x80) { start++; } return buf.slice(start).toString("utf-8"); } /** * Truncate a single line to max characters, adding [truncated] suffix. * Used for grep match lines. */ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) { if (line.length <= maxChars) { return { text: line, wasTruncated: false }; } return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true }; } /* v9-4a7ec19535f4405a */