1export const DEFAULT_MAX_LINES = 2000;
2export const DEFAULT_MAX_BYTES = 50 * 1024;
3export const GREP_MAX_LINE_LENGTH = 500;
4
5export interface TruncationResult {
6 content: string;
7 truncated: boolean;
8 truncatedBy: "lines" | "bytes" | null;
9 totalLines: number;
10 totalBytes: number;
11 outputLines: number;
12 outputBytes: number;
13 lastLinePartial: boolean;
14 firstLineExceedsLimit: boolean;
15 maxLines: number;
16 maxBytes: number;
17}
18
19export interface TruncationOptions {
20 maxLines?: number;
21 maxBytes?: number;
22}
23
24export function formatSize(bytes: number): string {
25 if (bytes < 1024) return `${bytes}B`;
26 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
27 return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
28}
29
30export function truncateHead(
31 content: string,
32 options: TruncationOptions = {},
33): TruncationResult {
34 const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
35 const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
36 const totalBytes = Buffer.byteLength(content, "utf-8");
37 const lines = content.split("\n");
38 const totalLines = lines.length;
39
40 if (totalLines <= maxLines && totalBytes <= maxBytes) {
41 return {
42 content,
43 truncated: false,
44 truncatedBy: null,
45 totalLines,
46 totalBytes,
47 outputLines: totalLines,
48 outputBytes: totalBytes,
49 lastLinePartial: false,
50 firstLineExceedsLimit: false,
51 maxLines,
52 maxBytes,
53 };
54 }
55
56 const firstLineBytes = Buffer.byteLength(lines[0]!, "utf-8");
57 if (firstLineBytes > maxBytes) {
58 return {
59 content: "",
60 truncated: true,
61 truncatedBy: "bytes",
62 totalLines,
63 totalBytes,
64 outputLines: 0,
65 outputBytes: 0,
66 lastLinePartial: false,
67 firstLineExceedsLimit: true,
68 maxLines,
69 maxBytes,
70 };
71 }
72
73 const outputLinesArr: string[] = [];
74 let outputBytesCount = 0;
75 let truncatedBy: "lines" | "bytes" = "lines";
76
77 for (let i = 0; i < lines.length && i < maxLines; i++) {
78 const line = lines[i]!;
79 const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
80 if (outputBytesCount + lineBytes > maxBytes) {
81 truncatedBy = "bytes";
82 break;
83 }
84 outputLinesArr.push(line);
85 outputBytesCount += lineBytes;
86 }
87
88 if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
89 truncatedBy = "lines";
90 }
91
92 const outputContent = outputLinesArr.join("\n");
93 const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
94
95 return {
96 content: outputContent,
97 truncated: true,
98 truncatedBy,
99 totalLines,
100 totalBytes,
101 outputLines: outputLinesArr.length,
102 outputBytes: finalOutputBytes,
103 lastLinePartial: false,
104 firstLineExceedsLimit: false,
105 maxLines,
106 maxBytes,
107 };
108}
109
110function truncateStringToBytesFromEnd(
111 str: string,
112 maxBytes: number,
113): string {
114 const buf = Buffer.from(str, "utf-8");
115 if (buf.length <= maxBytes) return str;
116 let end = buf.length;
117 let start = end - maxBytes;
118 // Walk forward to a valid UTF-8 boundary
119 while (start < end && (buf[start]! & 0xc0) === 0x80) {
120 start++;
121 }
122 return buf.subarray(start, end).toString("utf-8");
123}
124
125export function truncateTail(
126 content: string,
127 options: TruncationOptions = {},
128): TruncationResult {
129 const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
130 const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
131 const totalBytes = Buffer.byteLength(content, "utf-8");
132 const lines = content.split("\n");
133 const totalLines = lines.length;
134
135 if (totalLines <= maxLines && totalBytes <= maxBytes) {
136 return {
137 content,
138 truncated: false,
139 truncatedBy: null,
140 totalLines,
141 totalBytes,
142 outputLines: totalLines,
143 outputBytes: totalBytes,
144 lastLinePartial: false,
145 firstLineExceedsLimit: false,
146 maxLines,
147 maxBytes,
148 };
149 }
150
151 const outputLinesArr: string[] = [];
152 let outputBytesCount = 0;
153 let truncatedBy: "lines" | "bytes" = "lines";
154
155 for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
156 const line = lines[i]!;
157 const lineBytes =
158 Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
159 if (outputBytesCount + lineBytes > maxBytes) {
160 truncatedBy = "bytes";
161 break;
162 }
163 outputLinesArr.unshift(line);
164 outputBytesCount += lineBytes;
165 }
166
167 if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
168 truncatedBy = "lines";
169 }
170
171 let lastLinePartial = false;
172
173 // If we couldn't fit any lines (last line alone exceeds byte limit),
174 // take partial content from the end.
175 if (outputLinesArr.length === 0) {
176 const lastLine = lines[lines.length - 1]!;
177 const partial = truncateStringToBytesFromEnd(lastLine, maxBytes);
178 outputLinesArr.push(partial);
179 outputBytesCount = Buffer.byteLength(partial, "utf-8");
180 truncatedBy = "bytes";
181 lastLinePartial = true;
182 }
183
184 const outputContent = outputLinesArr.join("\n");
185 const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
186
187 return {
188 content: outputContent,
189 truncated: true,
190 truncatedBy,
191 totalLines,
192 totalBytes,
193 outputLines: outputLinesArr.length,
194 outputBytes: finalOutputBytes,
195 lastLinePartial,
196 firstLineExceedsLimit: false,
197 maxLines,
198 maxBytes,
199 };
200}
201
202export function truncateLine(
203 line: string,
204 maxChars: number = GREP_MAX_LINE_LENGTH,
205): { text: string; wasTruncated: boolean } {
206 if (line.length <= maxChars) {
207 return { text: line, wasTruncated: false };
208 }
209 return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
210}