1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
import { readFile } from "node:fs/promises";
import { z } from "zod";
import type { ToolDefinition } from "../types/index.js";
import { canonicalize } from "./path-utils.js";
import { SPILL_ROOT } from "./truncate.js";
const DEFAULT_LENGTH = 5000;
export function createReadFileSliceTool(workingDirectory: string): ToolDefinition {
return {
name: "read_file_slice",
description:
"Read a character-range slice of a single line in a file. Use this when read_file returns a truncated line marker like '[line N truncated, total X chars; use read_file_slice ...]', or when you need precise byte-ish access into a minified file (huge JSON, base64 blob, etc.). For normal line-oriented reading use read_file with offset/limit instead.",
parameters: z.object({
path: z.string().describe("Path to the file, relative to the working directory."),
line: z.number().int().min(1).describe("1-indexed line number to slice into."),
charOffset: z
.number()
.int()
.min(0)
.optional()
.describe("0-indexed character offset within the line. Default: 0 (start of line)."),
charLength: z
.number()
.int()
.min(1)
.optional()
.describe(
`Max characters to return from the slice. Default: ${DEFAULT_LENGTH}. The universal tool-output truncator will still spill if the result is huge.`,
),
}),
execute: async (args: Record<string, unknown>): Promise<string> => {
const filePath = args.path as string;
const lineNumber = args.line as number;
const charOffset =
typeof args.charOffset === "number" ? Math.max(0, Math.floor(args.charOffset)) : 0;
const charLength =
typeof args.charLength === "number"
? Math.max(1, Math.floor(args.charLength))
: DEFAULT_LENGTH;
// Canonicalize all three so symlink-in-workdir escapes are detected.
// See `canonicalize` in ./path-utils.ts for the resolution semantics.
const absolutePath = await canonicalize(workingDirectory, filePath);
const absoluteWorkDir = await canonicalize(workingDirectory);
const absoluteSpillRoot = await canonicalize(SPILL_ROOT);
const isUnderWorkdir =
absolutePath === absoluteWorkDir || absolutePath.startsWith(`${absoluteWorkDir}/`);
const isSpillFile =
absolutePath === absoluteSpillRoot || absolutePath.startsWith(`${absoluteSpillRoot}/`);
if (!isUnderWorkdir && !isSpillFile) {
return `Error: Path "${filePath}" is outside the working directory.`;
}
let raw: string;
try {
raw = await readFile(absolutePath, "utf8");
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
return `Error: File "${filePath}" not found.`;
}
return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
}
const lines = raw.split("\n");
const trailingNewline = raw.endsWith("\n");
const totalLines = trailingNewline ? lines.length - 1 : lines.length;
if (lineNumber > totalLines) {
return `Error: line ${lineNumber} exceeds file length (${totalLines} lines).`;
}
const line = lines[lineNumber - 1] ?? "";
const lineLength = line.length;
if (charOffset >= lineLength) {
return `Error: charOffset ${charOffset} exceeds line length (${lineLength} chars).`;
}
const sliceEnd = Math.min(charOffset + charLength, lineLength);
const slice = line.slice(charOffset, sliceEnd);
const remaining = lineLength - sliceEnd;
const header = `[file: ${filePath} — line ${lineNumber}, chars ${charOffset}-${sliceEnd} of ${lineLength}${remaining > 0 ? ` (${remaining.toLocaleString()} chars remain after this slice)` : ""}]`;
return `${header}\n${slice}`;
},
};
}
|