Files
smartagent/ts/smartagent.tools.filesystem.ts

424 lines
12 KiB
TypeScript
Raw Normal View History

2025-12-02 10:59:09 +00:00
import * as plugins from './plugins.js';
import * as interfaces from './smartagent.interfaces.js';
import { BaseToolWrapper } from './smartagent.tools.base.js';
2025-12-15 14:23:53 +00:00
/**
* Options for FilesystemTool
*/
export interface IFilesystemToolOptions {
/** Base path to scope all operations to. If set, all paths must be within this directory. */
basePath?: string;
}
2025-12-02 10:59:09 +00:00
/**
* Filesystem tool for file and directory operations
* Wraps @push.rocks/smartfs
*/
export class FilesystemTool extends BaseToolWrapper {
public name = 'filesystem';
public description = 'Read, write, list, and delete files and directories';
2025-12-15 14:23:53 +00:00
/** Base path to scope all operations to */
private basePath?: string;
constructor(options?: IFilesystemToolOptions) {
super();
if (options?.basePath) {
this.basePath = plugins.path.resolve(options.basePath);
}
}
/**
* Validate that a path is within the allowed base path
* @throws Error if path is outside allowed directory
*/
private validatePath(pathArg: string): string {
const resolved = plugins.path.resolve(pathArg);
if (this.basePath) {
// Ensure the resolved path starts with the base path
if (!resolved.startsWith(this.basePath + plugins.path.sep) && resolved !== this.basePath) {
throw new Error(`Access denied: path "${pathArg}" is outside allowed directory "${this.basePath}"`);
}
}
return resolved;
}
2025-12-02 10:59:09 +00:00
public actions: interfaces.IToolAction[] = [
{
name: 'read',
description: 'Read the contents of a file',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
encoding: {
type: 'string',
enum: ['utf8', 'binary', 'base64'],
default: 'utf8',
description: 'File encoding',
},
},
required: ['path'],
},
},
{
name: 'write',
description: 'Write content to a file (creates or overwrites)',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
content: { type: 'string', description: 'Content to write' },
encoding: {
type: 'string',
enum: ['utf8', 'binary', 'base64'],
default: 'utf8',
description: 'File encoding',
},
},
required: ['path', 'content'],
},
},
{
name: 'append',
description: 'Append content to a file',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
content: { type: 'string', description: 'Content to append' },
},
required: ['path', 'content'],
},
},
{
name: 'list',
description: 'List files and directories in a path',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Directory path to list' },
recursive: { type: 'boolean', default: false, description: 'List recursively' },
filter: { type: 'string', description: 'Glob pattern to filter results (e.g., "*.ts")' },
},
required: ['path'],
},
},
{
name: 'delete',
description: 'Delete a file or directory',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Path to delete' },
recursive: {
type: 'boolean',
default: false,
description: 'For directories, delete recursively',
},
},
required: ['path'],
},
},
{
name: 'exists',
description: 'Check if a file or directory exists',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Path to check' },
},
required: ['path'],
},
},
{
name: 'stat',
description: 'Get file or directory statistics (size, dates, etc.)',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Path to get stats for' },
},
required: ['path'],
},
},
{
name: 'copy',
description: 'Copy a file to a new location',
parameters: {
type: 'object',
properties: {
source: { type: 'string', description: 'Source file path' },
destination: { type: 'string', description: 'Destination file path' },
},
required: ['source', 'destination'],
},
},
{
name: 'move',
description: 'Move a file to a new location',
parameters: {
type: 'object',
properties: {
source: { type: 'string', description: 'Source file path' },
destination: { type: 'string', description: 'Destination file path' },
},
required: ['source', 'destination'],
},
},
{
name: 'mkdir',
description: 'Create a directory',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Directory path to create' },
recursive: {
type: 'boolean',
default: true,
description: 'Create parent directories if needed',
},
},
required: ['path'],
},
},
];
private smartfs!: plugins.smartfs.SmartFs;
public async initialize(): Promise<void> {
this.smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
this.isInitialized = true;
}
public async cleanup(): Promise<void> {
this.isInitialized = false;
}
public async execute(
action: string,
params: Record<string, unknown>
): Promise<interfaces.IToolExecutionResult> {
this.validateAction(action);
this.ensureInitialized();
try {
switch (action) {
case 'read': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
2025-12-02 10:59:09 +00:00
const encoding = (params.encoding as string) || 'utf8';
const content = await this.smartfs
2025-12-15 14:23:53 +00:00
.file(validatedPath)
2025-12-02 10:59:09 +00:00
.encoding(encoding as 'utf8' | 'binary' | 'base64')
.read();
return {
success: true,
result: {
path: params.path,
content: content.toString(),
encoding,
},
};
}
case 'write': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
2025-12-02 10:59:09 +00:00
const encoding = (params.encoding as string) || 'utf8';
await this.smartfs
2025-12-15 14:23:53 +00:00
.file(validatedPath)
2025-12-02 10:59:09 +00:00
.encoding(encoding as 'utf8' | 'binary' | 'base64')
.write(params.content as string);
return {
success: true,
result: {
path: params.path,
written: true,
bytesWritten: (params.content as string).length,
},
};
}
case 'append': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
await this.smartfs.file(validatedPath).append(params.content as string);
2025-12-02 10:59:09 +00:00
return {
success: true,
result: {
path: params.path,
appended: true,
},
};
}
case 'list': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
let dir = this.smartfs.directory(validatedPath);
2025-12-02 10:59:09 +00:00
if (params.recursive) {
dir = dir.recursive();
}
if (params.filter) {
dir = dir.filter(params.filter as string);
}
const entries = await dir.list();
return {
success: true,
result: {
path: params.path,
entries,
count: entries.length,
},
};
}
case 'delete': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
2025-12-02 10:59:09 +00:00
// Check if it's a directory or file
2025-12-15 14:23:53 +00:00
const exists = await this.smartfs.file(validatedPath).exists();
2025-12-02 10:59:09 +00:00
if (exists) {
// Try to get stats to check if it's a directory
try {
2025-12-15 14:23:53 +00:00
const stats = await this.smartfs.file(validatedPath).stat();
2025-12-02 10:59:09 +00:00
if (stats.isDirectory && params.recursive) {
2025-12-15 14:23:53 +00:00
await this.smartfs.directory(validatedPath).recursive().delete();
2025-12-02 10:59:09 +00:00
} else {
2025-12-15 14:23:53 +00:00
await this.smartfs.file(validatedPath).delete();
2025-12-02 10:59:09 +00:00
}
} catch {
2025-12-15 14:23:53 +00:00
await this.smartfs.file(validatedPath).delete();
2025-12-02 10:59:09 +00:00
}
}
return {
success: true,
result: {
2025-12-15 14:23:53 +00:00
path: params.path,
2025-12-02 10:59:09 +00:00
deleted: true,
},
};
}
case 'exists': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
const exists = await this.smartfs.file(validatedPath).exists();
2025-12-02 10:59:09 +00:00
return {
success: true,
result: {
path: params.path,
exists,
},
};
}
case 'stat': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
const stats = await this.smartfs.file(validatedPath).stat();
2025-12-02 10:59:09 +00:00
return {
success: true,
result: {
path: params.path,
stats,
},
};
}
case 'copy': {
2025-12-15 14:23:53 +00:00
const validatedSource = this.validatePath(params.source as string);
const validatedDest = this.validatePath(params.destination as string);
await this.smartfs.file(validatedSource).copy(validatedDest);
2025-12-02 10:59:09 +00:00
return {
success: true,
result: {
source: params.source,
destination: params.destination,
copied: true,
},
};
}
case 'move': {
2025-12-15 14:23:53 +00:00
const validatedSource = this.validatePath(params.source as string);
const validatedDest = this.validatePath(params.destination as string);
await this.smartfs.file(validatedSource).move(validatedDest);
2025-12-02 10:59:09 +00:00
return {
success: true,
result: {
source: params.source,
destination: params.destination,
moved: true,
},
};
}
case 'mkdir': {
2025-12-15 14:23:53 +00:00
const validatedPath = this.validatePath(params.path as string);
let dir = this.smartfs.directory(validatedPath);
2025-12-02 10:59:09 +00:00
if (params.recursive !== false) {
dir = dir.recursive();
}
await dir.create();
return {
success: true,
result: {
path: params.path,
created: true,
},
};
}
default:
return {
success: false,
error: `Unknown action: ${action}`,
};
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'read':
return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
case 'write': {
const content = params.content as string;
const preview = content.length > 100 ? content.substring(0, 100) + '...' : content;
return `Write ${content.length} bytes to "${params.path}". Content preview: "${preview}"`;
}
case 'append': {
const content = params.content as string;
const preview = content.length > 100 ? content.substring(0, 100) + '...' : content;
return `Append ${content.length} bytes to "${params.path}". Content preview: "${preview}"`;
}
case 'list':
return `List directory "${params.path}"${params.recursive ? ' recursively' : ''}${params.filter ? ` with filter "${params.filter}"` : ''}`;
case 'delete':
return `Delete "${params.path}"${params.recursive ? ' recursively' : ''}`;
case 'exists':
return `Check if "${params.path}" exists`;
case 'stat':
return `Get statistics for "${params.path}"`;
case 'copy':
return `Copy "${params.source}" to "${params.destination}"`;
case 'move':
return `Move "${params.source}" to "${params.destination}"`;
case 'mkdir':
return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
default:
return `Unknown action: ${action}`;
}
}
}