106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
/**
|
||
* Built-in file-read tool.
|
||
*
|
||
* Reads a file from disk and returns its contents with 1-based line numbers.
|
||
* Supports reading a slice of lines via `offset` and `limit` for large files.
|
||
*/
|
||
|
||
import { readFile } from 'fs/promises'
|
||
import { z } from 'zod'
|
||
import { defineTool } from '../framework.js'
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tool definition
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const fileReadTool = defineTool({
|
||
name: 'file_read',
|
||
description:
|
||
'Read the contents of a file from disk. ' +
|
||
'Returns the file contents with line numbers prefixed in the format "N\\t<line>". ' +
|
||
'Use `offset` and `limit` to read large files in chunks without loading the ' +
|
||
'entire file into the context window.',
|
||
|
||
inputSchema: z.object({
|
||
path: z.string().describe('Absolute path to the file to read.'),
|
||
offset: z
|
||
.number()
|
||
.int()
|
||
.nonnegative()
|
||
.optional()
|
||
.describe(
|
||
'1-based line number to start reading from. ' +
|
||
'When omitted the file is read from the beginning.',
|
||
),
|
||
limit: z
|
||
.number()
|
||
.int()
|
||
.positive()
|
||
.optional()
|
||
.describe(
|
||
'Maximum number of lines to return. ' +
|
||
'When omitted all lines from `offset` to the end of the file are returned.',
|
||
),
|
||
}),
|
||
|
||
execute: async (input) => {
|
||
let raw: string
|
||
try {
|
||
const buffer = await readFile(input.path)
|
||
raw = buffer.toString('utf8')
|
||
} catch (err) {
|
||
const message =
|
||
err instanceof Error ? err.message : 'Unknown error reading file.'
|
||
return {
|
||
data: `Could not read file "${input.path}": ${message}`,
|
||
isError: true,
|
||
}
|
||
}
|
||
|
||
// Split preserving trailing newlines correctly
|
||
const lines = raw.split('\n')
|
||
|
||
// Remove the last empty string produced by a trailing newline
|
||
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
||
lines.pop()
|
||
}
|
||
|
||
const totalLines = lines.length
|
||
|
||
// Apply offset (convert from 1-based to 0-based)
|
||
const startIndex =
|
||
input.offset !== undefined ? Math.max(0, input.offset - 1) : 0
|
||
|
||
if (startIndex >= totalLines && totalLines > 0) {
|
||
return {
|
||
data:
|
||
`File "${input.path}" has ${totalLines} line${totalLines === 1 ? '' : 's'} ` +
|
||
`but offset ${input.offset} is beyond the end.`,
|
||
isError: true,
|
||
}
|
||
}
|
||
|
||
const endIndex =
|
||
input.limit !== undefined
|
||
? Math.min(startIndex + input.limit, totalLines)
|
||
: totalLines
|
||
|
||
const slice = lines.slice(startIndex, endIndex)
|
||
|
||
// Build line-numbered output (1-based line numbers matching file positions)
|
||
const numbered = slice
|
||
.map((line, i) => `${startIndex + i + 1}\t${line}`)
|
||
.join('\n')
|
||
|
||
const meta =
|
||
endIndex < totalLines
|
||
? `\n\n(showing lines ${startIndex + 1}–${endIndex} of ${totalLines})`
|
||
: ''
|
||
|
||
return {
|
||
data: numbered + meta,
|
||
isError: false,
|
||
}
|
||
},
|
||
})
|