- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
311 lines
7.9 KiB
JavaScript
311 lines
7.9 KiB
JavaScript
/**
|
|
* File Operation Tool
|
|
* Handle file system operations safely
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { BaseTool, ToolResult } = require('./tool-base.cjs');
|
|
|
|
class FileOperationTool extends BaseTool {
|
|
constructor(config = {}) {
|
|
super({
|
|
name: 'file',
|
|
description: 'Perform file system operations (read, write, list, etc.)',
|
|
parameters: [
|
|
{
|
|
name: 'operation',
|
|
type: 'string',
|
|
required: true,
|
|
description: 'Operation to perform: read, write, list, delete, move, copy, exists, stat'
|
|
},
|
|
{
|
|
name: 'path',
|
|
type: 'string',
|
|
required: false,
|
|
description: 'File or directory path'
|
|
},
|
|
{
|
|
name: 'content',
|
|
type: 'string',
|
|
required: false,
|
|
description: 'Content for write operations'
|
|
},
|
|
{
|
|
name: 'destination',
|
|
type: 'string',
|
|
required: false,
|
|
description: 'Destination path for move/copy operations'
|
|
},
|
|
{
|
|
name: 'encoding',
|
|
type: 'string',
|
|
required: false,
|
|
description: 'File encoding (default: utf8)'
|
|
}
|
|
],
|
|
...config
|
|
});
|
|
|
|
this.allowedPaths = config.allowedPaths || [];
|
|
this.maxFileSize = config.maxFileSize || 1024 * 1024; // 1MB
|
|
}
|
|
|
|
async execute(params) {
|
|
const { operation, path: filePath, content, destination, encoding = 'utf8' } = params;
|
|
|
|
// Validate path
|
|
const validation = this.validatePath(filePath);
|
|
if (!validation.valid) {
|
|
throw new Error(`Path validation failed: ${validation.reason}`);
|
|
}
|
|
|
|
try {
|
|
switch (operation) {
|
|
case 'read':
|
|
return await this.readFile(filePath, encoding);
|
|
|
|
case 'write':
|
|
return await this.writeFile(filePath, content, encoding);
|
|
|
|
case 'list':
|
|
return await this.listFiles(filePath);
|
|
|
|
case 'delete':
|
|
return await this.deleteFile(filePath);
|
|
|
|
case 'move':
|
|
return await this.moveFile(filePath, destination);
|
|
|
|
case 'copy':
|
|
return await this.copyFile(filePath, destination);
|
|
|
|
case 'exists':
|
|
return await this.fileExists(filePath);
|
|
|
|
case 'stat':
|
|
return await this.getFileStats(filePath);
|
|
|
|
default:
|
|
throw new Error(`Unknown operation: ${operation}`);
|
|
}
|
|
} catch (error) {
|
|
return ToolResult.failure(
|
|
error,
|
|
`File operation '${operation}' failed: ${error.message}`,
|
|
{ operation, path: filePath }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate file path against security rules
|
|
*/
|
|
validatePath(filePath) {
|
|
if (!filePath) {
|
|
return { valid: false, reason: 'Path is required' };
|
|
}
|
|
|
|
// Resolve absolute path
|
|
const resolvedPath = path.resolve(filePath);
|
|
|
|
// Check against allowed paths if configured
|
|
if (this.allowedPaths.length > 0) {
|
|
const isAllowed = this.allowedPaths.some(allowedPath => {
|
|
const resolvedAllowed = path.resolve(allowedPath);
|
|
return resolvedPath.startsWith(resolvedAllowed);
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
return { valid: false, reason: 'Path is outside allowed directories' };
|
|
}
|
|
}
|
|
|
|
// Prevent path traversal
|
|
if (filePath.includes('..')) {
|
|
return { valid: false, reason: 'Path traversal not allowed' };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
async readFile(filePath, encoding) {
|
|
const stats = await fs.stat(filePath);
|
|
|
|
if (stats.size > this.maxFileSize) {
|
|
throw new Error(`File too large (${stats.size} bytes, max ${this.maxFileSize})`);
|
|
}
|
|
|
|
if (!stats.isFile()) {
|
|
throw new Error('Path is not a file');
|
|
}
|
|
|
|
const content = await fs.readFile(filePath, encoding);
|
|
|
|
return ToolResult.success(
|
|
{ content, size: stats.size },
|
|
content,
|
|
{ operation: 'read', path: filePath, size: stats.size }
|
|
);
|
|
}
|
|
|
|
async writeFile(filePath, content, encoding) {
|
|
if (content === undefined || content === null) {
|
|
throw new Error('Content is required for write operation');
|
|
}
|
|
|
|
// Create parent directories if needed
|
|
const dir = path.dirname(filePath);
|
|
await fs.mkdir(dir, { recursive: true });
|
|
|
|
await fs.writeFile(filePath, content, encoding);
|
|
|
|
const stats = await fs.stat(filePath);
|
|
|
|
return ToolResult.success(
|
|
{ size: stats.size },
|
|
`Wrote ${stats.size} bytes to ${filePath}`,
|
|
{ operation: 'write', path: filePath, size: stats.size }
|
|
);
|
|
}
|
|
|
|
async listFiles(dirPath) {
|
|
const stats = await fs.stat(dirPath);
|
|
|
|
if (!stats.isDirectory()) {
|
|
throw new Error('Path is not a directory');
|
|
}
|
|
|
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
|
|
const files = entries.map(entry => ({
|
|
name: entry.name,
|
|
type: entry.isDirectory() ? 'directory' : 'file',
|
|
path: path.join(dirPath, entry.name)
|
|
}));
|
|
|
|
const output = files
|
|
.map(f => `${f.type === 'directory' ? 'D' : 'F'} ${f.name}`)
|
|
.join('\n');
|
|
|
|
return ToolResult.success(
|
|
files,
|
|
output || '[Empty directory]',
|
|
{ operation: 'list', path: dirPath, count: files.length }
|
|
);
|
|
}
|
|
|
|
async deleteFile(filePath) {
|
|
const stats = await fs.stat(filePath);
|
|
|
|
if (stats.isDirectory()) {
|
|
await fs.rmdir(filePath, { recursive: true });
|
|
} else {
|
|
await fs.unlink(filePath);
|
|
}
|
|
|
|
return ToolResult.success(
|
|
{ deleted: true },
|
|
`Deleted: ${filePath}`,
|
|
{ operation: 'delete', path: filePath }
|
|
);
|
|
}
|
|
|
|
async moveFile(source, destination) {
|
|
if (!destination) {
|
|
throw new Error('Destination is required for move operation');
|
|
}
|
|
|
|
const destValidation = this.validatePath(destination);
|
|
if (!destValidation.valid) {
|
|
throw new Error(`Destination validation failed: ${destValidation.reason}`);
|
|
}
|
|
|
|
// Create parent directories
|
|
const destDir = path.dirname(destination);
|
|
await fs.mkdir(destDir, { recursive: true });
|
|
|
|
await fs.rename(source, destination);
|
|
|
|
return ToolResult.success(
|
|
{ moved: true },
|
|
`Moved ${source} to ${destination}`,
|
|
{ operation: 'move', source, destination }
|
|
);
|
|
}
|
|
|
|
async copyFile(source, destination) {
|
|
if (!destination) {
|
|
throw new Error('Destination is required for copy operation');
|
|
}
|
|
|
|
const destValidation = this.validatePath(destination);
|
|
if (!destValidation.valid) {
|
|
throw new Error(`Destination validation failed: ${destValidation.reason}`);
|
|
}
|
|
|
|
// Create parent directories
|
|
const destDir = path.dirname(destination);
|
|
await fs.mkdir(destDir, { recursive: true });
|
|
|
|
await fs.copyFile(source, destination);
|
|
|
|
const stats = await fs.stat(destination);
|
|
|
|
return ToolResult.success(
|
|
{ size: stats.size },
|
|
`Copied ${source} to ${destination}`,
|
|
{ operation: 'copy', source, destination, size: stats.size }
|
|
);
|
|
}
|
|
|
|
async fileExists(filePath) {
|
|
try {
|
|
await fs.access(filePath);
|
|
return ToolResult.success(
|
|
{ exists: true },
|
|
`File exists: ${filePath}`,
|
|
{ operation: 'exists', path: filePath, exists: true }
|
|
);
|
|
} catch {
|
|
return ToolResult.success(
|
|
{ exists: false },
|
|
`File does not exist: ${filePath}`,
|
|
{ operation: 'exists', path: filePath, exists: false }
|
|
);
|
|
}
|
|
}
|
|
|
|
async getFileStats(filePath) {
|
|
const stats = await fs.stat(filePath);
|
|
|
|
const info = {
|
|
size: stats.size,
|
|
created: stats.birthtime,
|
|
modified: stats.mtime,
|
|
accessed: stats.atime,
|
|
isFile: stats.isFile(),
|
|
isDirectory: stats.isDirectory(),
|
|
permissions: stats.mode.toString(8)
|
|
};
|
|
|
|
const output = `
|
|
Size: ${info.size} bytes
|
|
Created: ${info.created}
|
|
Modified: ${info.modified}
|
|
Type: ${info.isFile ? 'File' : 'Directory'}
|
|
Permissions: ${info.permissions}
|
|
`.trim();
|
|
|
|
return ToolResult.success(
|
|
info,
|
|
output,
|
|
{ operation: 'stat', path: filePath }
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
FileOperationTool
|
|
};
|