Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
7.3 kB · 258 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259/** * 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). */
export const DEFAULT_MAX_LINES = 2000;export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KBexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
export interface TruncationResult { /** The truncated content */ content: string; /** Whether truncation occurred */ truncated: boolean; /** Which limit was hit: "lines", "bytes", or null if not truncated */ truncatedBy: "lines" | "bytes" | null; /** Total number of lines in the original content */ totalLines: number; /** Total number of bytes in the original content */ totalBytes: number; /** Number of complete lines in the truncated output */ outputLines: number; /** Number of bytes in the truncated output */ outputBytes: number; /** Whether the last line was partially truncated (only for tail truncation edge case) */ lastLinePartial: boolean; /** Whether the first line exceeded the byte limit (for head truncation) */ firstLineExceedsLimit: boolean; /** The max lines limit that was applied */ maxLines: number; /** The max bytes limit that was applied */ maxBytes: number;}
export interface TruncationOptions { /** Maximum number of lines (default: 2000) */ maxLines?: number; /** Maximum number of bytes (default: 50KB) */ maxBytes?: number;}
/** * Format bytes as human-readable size. */export function formatSize(bytes: number): string { 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. */export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult { 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;
if (totalLines <= maxLines && totalBytes <= maxBytes) { return { content, truncated: false, truncatedBy: null, totalLines, totalBytes, outputLines: totalLines, outputBytes: totalBytes, lastLinePartial: false, firstLineExceedsLimit: false, maxLines, maxBytes, }; }
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, }; }
const outputLinesArr: string[] = []; let outputBytesCount = 0; let truncatedBy: "lines" | "bytes" = "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 (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. */export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult { 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;
if (totalLines <= maxLines && totalBytes <= maxBytes) { return { content, truncated: false, truncatedBy: null, totalLines, totalBytes, outputLines: totalLines, outputBytes: totalBytes, lastLinePartial: false, firstLineExceedsLimit: false, maxLines, maxBytes, }; }
const outputLinesArr: string[] = []; let outputBytesCount = 0; let truncatedBy: "lines" | "bytes" = "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"; // Trailing blanks must not defeat the oversized-line rescue; keep as many as the budget allows. if (outputLinesArr.every((collected) => collected.length === 0)) { const keptBlanks = Math.min(outputLinesArr.length, Math.max(0, maxBytes - 1)); outputLinesArr.length = keptBlanks; const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes - keptBlanks); outputLinesArr.unshift(truncatedLine); outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8") + keptBlanks; lastLinePartial = true; } break; }
outputLinesArr.unshift(line); outputBytesCount += lineBytes; }
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: string, maxBytes: number): string { const buf = Buffer.from(str, "utf-8"); if (buf.length <= maxBytes) { return str; }
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. */export function truncateLine( line: string, maxChars: number = GREP_MAX_LINE_LENGTH,): { text: string; wasTruncated: boolean } { if (line.length <= maxChars) { return { text: line, wasTruncated: false }; } return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };}