132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
|
|
export interface IFilesystemToolOptions {
|
|
/** Restrict file access to this directory. Default: process.cwd() */
|
|
rootDir?: string;
|
|
}
|
|
|
|
function validatePath(filePath: string, rootDir?: string): string {
|
|
const resolved = plugins.path.resolve(filePath);
|
|
if (rootDir) {
|
|
const resolvedRoot = plugins.path.resolve(rootDir);
|
|
if (!resolved.startsWith(resolvedRoot + plugins.path.sep) && resolved !== resolvedRoot) {
|
|
throw new Error(`Access denied: "${filePath}" is outside allowed root "${rootDir}"`);
|
|
}
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
export function filesystemTool(options?: IFilesystemToolOptions): plugins.ToolSet {
|
|
const rootDir = options?.rootDir;
|
|
|
|
return {
|
|
read_file: plugins.tool({
|
|
description:
|
|
'Read file contents. Returns the full text or a specified line range.',
|
|
inputSchema: plugins.z.object({
|
|
path: plugins.z.string().describe('Absolute path to the file'),
|
|
startLine: plugins.z
|
|
.number()
|
|
.optional()
|
|
.describe('First line (1-indexed, inclusive)'),
|
|
endLine: plugins.z
|
|
.number()
|
|
.optional()
|
|
.describe('Last line (1-indexed, inclusive)'),
|
|
}),
|
|
execute: async ({
|
|
path: filePath,
|
|
startLine,
|
|
endLine,
|
|
}: {
|
|
path: string;
|
|
startLine?: number;
|
|
endLine?: number;
|
|
}) => {
|
|
const resolved = validatePath(filePath, rootDir);
|
|
const content = plugins.fs.readFileSync(resolved, 'utf-8');
|
|
|
|
if (startLine !== undefined || endLine !== undefined) {
|
|
const lines = content.split('\n');
|
|
const start = (startLine ?? 1) - 1;
|
|
const end = endLine ?? lines.length;
|
|
const sliced = lines.slice(start, end).join('\n');
|
|
return plugins.truncateOutput(sliced).content;
|
|
}
|
|
|
|
return plugins.truncateOutput(content).content;
|
|
},
|
|
}),
|
|
|
|
write_file: plugins.tool({
|
|
description:
|
|
'Write content to a file (creates parent dirs if needed, overwrites existing).',
|
|
inputSchema: plugins.z.object({
|
|
path: plugins.z.string().describe('Absolute path to the file'),
|
|
content: plugins.z.string().describe('Content to write'),
|
|
}),
|
|
execute: async ({ path: filePath, content }: { path: string; content: string }) => {
|
|
const resolved = validatePath(filePath, rootDir);
|
|
const dir = plugins.path.dirname(resolved);
|
|
plugins.fs.mkdirSync(dir, { recursive: true });
|
|
plugins.fs.writeFileSync(resolved, content, 'utf-8');
|
|
return `Written ${content.length} characters to ${filePath}`;
|
|
},
|
|
}),
|
|
|
|
list_directory: plugins.tool({
|
|
description: 'List files and directories at the given path.',
|
|
inputSchema: plugins.z.object({
|
|
path: plugins.z.string().describe('Directory path to list'),
|
|
recursive: plugins.z
|
|
.boolean()
|
|
.optional()
|
|
.describe('List recursively (default: false)'),
|
|
}),
|
|
execute: async ({
|
|
path: dirPath,
|
|
recursive,
|
|
}: {
|
|
path: string;
|
|
recursive?: boolean;
|
|
}) => {
|
|
const resolved = validatePath(dirPath, rootDir);
|
|
|
|
function listDir(dir: string, prefix: string = ''): string[] {
|
|
const entries = plugins.fs.readdirSync(dir, { withFileTypes: true });
|
|
const result: string[] = [];
|
|
for (const entry of entries) {
|
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
const indicator = entry.isDirectory() ? '/' : '';
|
|
result.push(`${rel}${indicator}`);
|
|
if (recursive && entry.isDirectory()) {
|
|
result.push(...listDir(plugins.path.join(dir, entry.name), rel));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const entries = listDir(resolved);
|
|
return plugins.truncateOutput(entries.join('\n')).content;
|
|
},
|
|
}),
|
|
|
|
delete_file: plugins.tool({
|
|
description: 'Delete a file or empty directory.',
|
|
inputSchema: plugins.z.object({
|
|
path: plugins.z.string().describe('Path to delete'),
|
|
}),
|
|
execute: async ({ path: filePath }: { path: string }) => {
|
|
const resolved = validatePath(filePath, rootDir);
|
|
const stat = plugins.fs.statSync(resolved);
|
|
if (stat.isDirectory()) {
|
|
plugins.fs.rmdirSync(resolved);
|
|
} else {
|
|
plugins.fs.unlinkSync(resolved);
|
|
}
|
|
return `Deleted ${filePath}`;
|
|
},
|
|
}),
|
|
};
|
|
}
|