Files
harness-tools/extensions/file-search/src/output.ts
T

127 lines
3.6 KiB
TypeScript

/**
* Shared output shaping for the fd and rg tools: standard pi truncation
* (2000 lines / 50KB) with the full output persisted to a temp file when
* anything is cut off.
*/
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
DEFAULT_MAX_BYTES,
DEFAULT_MAX_LINES,
formatSize,
truncateHead,
} from "@earendil-works/pi-coding-agent";
export interface FormattedOutput {
readonly text: string;
readonly lineCount: number;
readonly truncated: boolean;
readonly fullOutputPath?: string;
}
export interface CapturedOutput {
readonly preview: string;
readonly lineCount: number;
readonly totalBytes: number;
readonly truncated: boolean;
readonly fullOutputPath?: string;
}
export interface FormatOutputOptions {
/** Temp-file prefix, e.g. "pi-fd-". */
readonly tempPrefix: string;
/** Injectable for tests. */
readonly persistFullOutput?: (output: string) => Promise<string>;
}
async function persistToTempFile(prefix: string, output: string) {
const directory = await mkdtemp(join(tmpdir(), prefix));
const path = join(directory, "output.txt");
await writeFile(path, output, "utf8");
return path;
}
function truncationNotice(options: {
content: string;
outputLines: number;
totalLines: number;
outputBytes: number;
totalBytes: number;
fullOutputPath: string;
}) {
return (
`${options.content}\n\n[Output truncated: ${options.outputLines} of ${options.totalLines} lines ` +
`(${formatSize(options.outputBytes)} of ${formatSize(options.totalBytes)}). ` +
`Full output saved to: ${options.fullOutputPath}]`
);
}
/** Format output already captured by a bounded-memory streaming process. */
export function formatCapturedOutput(captured: CapturedOutput) {
const trimmed = captured.preview.replace(/\n+$/, "");
if (!captured.truncated || !captured.fullOutputPath) {
return {
text: trimmed,
lineCount: captured.lineCount,
truncated: false,
} satisfies FormattedOutput;
}
const truncation = truncateHead(trimmed, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
const content = truncation.content;
const outputLines = content === "" ? 0 : content.split("\n").length;
const outputBytes = Buffer.byteLength(content);
return {
text: truncationNotice({
content,
outputLines,
totalLines: captured.lineCount,
outputBytes,
totalBytes: captured.totalBytes,
fullOutputPath: captured.fullOutputPath,
}),
lineCount: captured.lineCount,
truncated: true,
fullOutputPath: captured.fullOutputPath,
} satisfies FormattedOutput;
}
/** Truncate to pi's standard limits, persisting the full output when cut. */
export async function formatOutput(
output: string,
options: FormatOutputOptions,
): Promise<FormattedOutput> {
const trimmed = output.replace(/\n+$/, "");
const lineCount = trimmed === "" ? 0 : trimmed.split("\n").length;
const truncation = truncateHead(trimmed, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
if (!truncation.truncated) {
return { text: trimmed, lineCount, truncated: false };
}
const persist =
options.persistFullOutput ??
((full: string) => persistToTempFile(options.tempPrefix, full));
const fullOutputPath = await persist(trimmed);
const text = truncationNotice({
content: truncation.content,
outputLines: truncation.outputLines,
totalLines: truncation.totalLines,
outputBytes: truncation.outputBytes,
totalBytes: truncation.totalBytes,
fullOutputPath,
});
return { text, lineCount, truncated: true, fullOutputPath };
}