Files
admin b723e2bd7d Reorganize: Move all skills to skills/ folder
- 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>
2026-01-23 18:05:17 +00:00

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
};