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',
|
2025-12-15 14:49:26 +00:00
|
|
|
description: 'Read file contents (full or specific line range)',
|
2025-12-02 10:59:09 +00:00
|
|
|
parameters: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
2025-12-15 14:49:26 +00:00
|
|
|
path: { type: 'string', description: 'Path to the file' },
|
2025-12-02 10:59:09 +00:00
|
|
|
encoding: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
enum: ['utf8', 'binary', 'base64'],
|
|
|
|
|
default: 'utf8',
|
|
|
|
|
description: 'File encoding',
|
|
|
|
|
},
|
2025-12-15 14:49:26 +00:00
|
|
|
startLine: {
|
|
|
|
|
type: 'number',
|
|
|
|
|
description: 'First line to read (1-indexed, inclusive). If omitted, reads from beginning.',
|
|
|
|
|
},
|
|
|
|
|
endLine: {
|
|
|
|
|
type: 'number',
|
|
|
|
|
description: 'Last line to read (1-indexed, inclusive). If omitted, reads to end.',
|
|
|
|
|
},
|
2025-12-02 10:59:09 +00:00
|
|
|
},
|
|
|
|
|
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'],
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-12-15 14:49:26 +00:00
|
|
|
{
|
|
|
|
|
name: 'tree',
|
|
|
|
|
description: 'Show directory structure as a tree (no file contents)',
|
|
|
|
|
parameters: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
path: { type: 'string', description: 'Root directory path' },
|
|
|
|
|
maxDepth: {
|
|
|
|
|
type: 'number',
|
|
|
|
|
default: 3,
|
|
|
|
|
description: 'Maximum depth to traverse (default: 3)',
|
|
|
|
|
},
|
|
|
|
|
filter: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: 'Glob pattern to filter files (e.g., "*.ts")',
|
|
|
|
|
},
|
|
|
|
|
showSizes: {
|
|
|
|
|
type: 'boolean',
|
|
|
|
|
default: false,
|
|
|
|
|
description: 'Include file sizes in output',
|
|
|
|
|
},
|
|
|
|
|
format: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
enum: ['string', 'json'],
|
|
|
|
|
default: 'string',
|
|
|
|
|
description: 'Output format: "string" for human-readable tree, "json" for structured array',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
required: ['path'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'glob',
|
|
|
|
|
description: 'Find files matching a glob pattern',
|
|
|
|
|
parameters: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
pattern: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.js")',
|
|
|
|
|
},
|
|
|
|
|
path: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: 'Base path to search from (defaults to current directory)',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
required: ['pattern'],
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-12-02 10:59:09 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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';
|
2025-12-15 14:49:26 +00:00
|
|
|
const startLine = params.startLine as number | undefined;
|
|
|
|
|
const endLine = params.endLine as number | undefined;
|
|
|
|
|
|
|
|
|
|
const fullContent = 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();
|
2025-12-15 14:49:26 +00:00
|
|
|
|
|
|
|
|
const contentStr = fullContent.toString();
|
|
|
|
|
const lines = contentStr.split('\n');
|
|
|
|
|
const totalLines = lines.length;
|
|
|
|
|
|
|
|
|
|
// Apply line range if specified
|
|
|
|
|
let resultContent: string;
|
|
|
|
|
let resultStartLine = 1;
|
|
|
|
|
let resultEndLine = totalLines;
|
|
|
|
|
|
|
|
|
|
if (startLine !== undefined || endLine !== undefined) {
|
|
|
|
|
const start = Math.max(1, startLine ?? 1);
|
|
|
|
|
const end = Math.min(totalLines, endLine ?? totalLines);
|
|
|
|
|
resultStartLine = start;
|
|
|
|
|
resultEndLine = end;
|
|
|
|
|
|
|
|
|
|
// Convert to 0-indexed for array slicing
|
|
|
|
|
const selectedLines = lines.slice(start - 1, end);
|
|
|
|
|
|
|
|
|
|
// Add line numbers to output for context
|
|
|
|
|
resultContent = selectedLines
|
|
|
|
|
.map((line, idx) => `${String(start + idx).padStart(5)}│ ${line}`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
} else {
|
|
|
|
|
// No range specified - return full content but warn if large
|
|
|
|
|
const MAX_LINES_WITHOUT_RANGE = 500;
|
|
|
|
|
if (totalLines > MAX_LINES_WITHOUT_RANGE) {
|
|
|
|
|
// Return first portion with warning
|
|
|
|
|
const selectedLines = lines.slice(0, MAX_LINES_WITHOUT_RANGE);
|
|
|
|
|
resultContent = selectedLines
|
|
|
|
|
.map((line, idx) => `${String(idx + 1).padStart(5)}│ ${line}`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
resultContent += `\n\n[... ${totalLines - MAX_LINES_WITHOUT_RANGE} more lines. Use startLine/endLine to read specific ranges.]`;
|
|
|
|
|
resultEndLine = MAX_LINES_WITHOUT_RANGE;
|
|
|
|
|
} else {
|
|
|
|
|
resultContent = contentStr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:59:09 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
result: {
|
|
|
|
|
path: params.path,
|
2025-12-15 14:49:26 +00:00
|
|
|
content: resultContent,
|
2025-12-02 10:59:09 +00:00
|
|
|
encoding,
|
2025-12-15 14:49:26 +00:00
|
|
|
totalLines,
|
|
|
|
|
startLine: resultStartLine,
|
|
|
|
|
endLine: resultEndLine,
|
2025-12-02 10:59:09 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 14:49:26 +00:00
|
|
|
case 'tree': {
|
|
|
|
|
const validatedPath = this.validatePath(params.path as string);
|
|
|
|
|
const maxDepth = (params.maxDepth as number) ?? 3;
|
|
|
|
|
const filter = params.filter as string | undefined;
|
|
|
|
|
const showSizes = (params.showSizes as boolean) ?? false;
|
|
|
|
|
const format = (params.format as 'string' | 'json') ?? 'string';
|
|
|
|
|
|
|
|
|
|
// Collect all entries recursively up to maxDepth
|
|
|
|
|
interface ITreeEntry {
|
|
|
|
|
path: string;
|
|
|
|
|
relativePath: string;
|
|
|
|
|
isDir: boolean;
|
|
|
|
|
depth: number;
|
|
|
|
|
size?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const entries: ITreeEntry[] = [];
|
|
|
|
|
|
|
|
|
|
const collectEntries = async (dirPath: string, depth: number, relativePath: string) => {
|
|
|
|
|
if (depth > maxDepth) return;
|
|
|
|
|
|
|
|
|
|
let dir = this.smartfs.directory(dirPath);
|
|
|
|
|
if (filter) {
|
|
|
|
|
dir = dir.filter(filter);
|
|
|
|
|
}
|
|
|
|
|
const items = await dir.list();
|
|
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const itemPath = plugins.path.join(dirPath, item);
|
|
|
|
|
const itemRelPath = relativePath ? `${relativePath}/${item}` : item;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const stats = await this.smartfs.file(itemPath).stat();
|
|
|
|
|
const isDir = stats.isDirectory;
|
|
|
|
|
|
|
|
|
|
const entry: ITreeEntry = {
|
|
|
|
|
path: itemPath,
|
|
|
|
|
relativePath: itemRelPath,
|
|
|
|
|
isDir,
|
|
|
|
|
depth,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (showSizes && !isDir) {
|
|
|
|
|
entry.size = stats.size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries.push(entry);
|
|
|
|
|
|
|
|
|
|
// Recurse into directories
|
|
|
|
|
if (isDir && depth < maxDepth) {
|
|
|
|
|
await collectEntries(itemPath, depth + 1, itemRelPath);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Skip items we can't stat
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await collectEntries(validatedPath, 0, '');
|
|
|
|
|
|
|
|
|
|
// Sort entries by path for consistent output
|
|
|
|
|
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
|
|
|
|
|
|
|
|
if (format === 'json') {
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
result: {
|
|
|
|
|
path: params.path,
|
|
|
|
|
entries: entries.map((e) => ({
|
|
|
|
|
path: e.relativePath,
|
|
|
|
|
isDir: e.isDir,
|
|
|
|
|
depth: e.depth,
|
|
|
|
|
...(e.size !== undefined ? { size: e.size } : {}),
|
|
|
|
|
})),
|
|
|
|
|
count: entries.length,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format as string tree
|
|
|
|
|
const formatSize = (bytes: number): string => {
|
|
|
|
|
if (bytes < 1024) return `${bytes}B`;
|
|
|
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build tree string with proper indentation
|
|
|
|
|
let treeStr = `${params.path}/\n`;
|
|
|
|
|
const pathParts = new Map<string, number>(); // Track which paths are last in their parent
|
|
|
|
|
|
|
|
|
|
// Group by parent to determine last child
|
|
|
|
|
const parentChildCount = new Map<string, number>();
|
|
|
|
|
const parentCurrentChild = new Map<string, number>();
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const parentPath = entry.relativePath.includes('/')
|
|
|
|
|
? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
|
|
|
|
|
: '';
|
|
|
|
|
parentChildCount.set(parentPath, (parentChildCount.get(parentPath) || 0) + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const parentPath = entry.relativePath.includes('/')
|
|
|
|
|
? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
|
|
|
|
|
: '';
|
|
|
|
|
parentCurrentChild.set(parentPath, (parentCurrentChild.get(parentPath) || 0) + 1);
|
|
|
|
|
const isLast = parentCurrentChild.get(parentPath) === parentChildCount.get(parentPath);
|
|
|
|
|
|
|
|
|
|
// Build prefix based on depth
|
|
|
|
|
let prefix = '';
|
|
|
|
|
const parts = entry.relativePath.split('/');
|
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
|
|
|
prefix += '│ ';
|
|
|
|
|
}
|
|
|
|
|
prefix += isLast ? '└── ' : '├── ';
|
|
|
|
|
|
|
|
|
|
const name = parts[parts.length - 1];
|
|
|
|
|
const suffix = entry.isDir ? '/' : '';
|
|
|
|
|
const sizeStr = showSizes && entry.size !== undefined ? ` (${formatSize(entry.size)})` : '';
|
|
|
|
|
|
|
|
|
|
treeStr += `${prefix}${name}${suffix}${sizeStr}\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
result: {
|
|
|
|
|
path: params.path,
|
|
|
|
|
tree: treeStr,
|
|
|
|
|
count: entries.length,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'glob': {
|
|
|
|
|
const pattern = params.pattern as string;
|
|
|
|
|
const basePath = params.path ? this.validatePath(params.path as string) : (this.basePath || process.cwd());
|
|
|
|
|
|
|
|
|
|
// Use smartfs to list with filter
|
|
|
|
|
const dir = this.smartfs.directory(basePath).recursive().filter(pattern);
|
|
|
|
|
const matches = await dir.list();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
result: {
|
|
|
|
|
pattern,
|
|
|
|
|
basePath,
|
|
|
|
|
matches,
|
|
|
|
|
count: matches.length,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:59:09 +00:00
|
|
|
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) {
|
2025-12-15 14:49:26 +00:00
|
|
|
case 'read': {
|
|
|
|
|
const lineRange = params.startLine || params.endLine
|
|
|
|
|
? ` lines ${params.startLine || 1}-${params.endLine || 'end'}`
|
|
|
|
|
: '';
|
|
|
|
|
return `Read file "${params.path}"${lineRange}`;
|
|
|
|
|
}
|
2025-12-02 10:59:09 +00:00
|
|
|
|
|
|
|
|
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)' : ''}`;
|
|
|
|
|
|
2025-12-15 14:49:26 +00:00
|
|
|
case 'tree':
|
|
|
|
|
return `Show tree of "${params.path}" (depth: ${params.maxDepth ?? 3}, format: ${params.format ?? 'string'})`;
|
|
|
|
|
|
|
|
|
|
case 'glob':
|
|
|
|
|
return `Find files matching "${params.pattern}"${params.path ? ` in "${params.path}"` : ''}`;
|
|
|
|
|
|
2025-12-02 10:59:09 +00:00
|
|
|
default:
|
|
|
|
|
return `Unknown action: ${action}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|