This commit is contained in:
2025-12-15 14:49:26 +00:00
parent 9e848045f7
commit 8662b73adb
5 changed files with 489 additions and 43 deletions

View File

@@ -46,17 +46,25 @@ export class FilesystemTool extends BaseToolWrapper {
public actions: interfaces.IToolAction[] = [
{
name: 'read',
description: 'Read the contents of a file',
description: 'Read file contents (full or specific line range)',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
path: { type: 'string', description: 'Path to the file' },
encoding: {
type: 'string',
enum: ['utf8', 'binary', 'base64'],
default: 'utf8',
description: 'File encoding',
},
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.',
},
},
required: ['path'],
},
@@ -182,6 +190,55 @@ export class FilesystemTool extends BaseToolWrapper {
required: ['path'],
},
},
{
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'],
},
},
];
private smartfs!: plugins.smartfs.SmartFs;
@@ -207,16 +264,61 @@ export class FilesystemTool extends BaseToolWrapper {
case 'read': {
const validatedPath = this.validatePath(params.path as string);
const encoding = (params.encoding as string) || 'utf8';
const content = await this.smartfs
const startLine = params.startLine as number | undefined;
const endLine = params.endLine as number | undefined;
const fullContent = await this.smartfs
.file(validatedPath)
.encoding(encoding as 'utf8' | 'binary' | 'base64')
.read();
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;
}
}
return {
success: true,
result: {
path: params.path,
content: content.toString(),
content: resultContent,
encoding,
totalLines,
startLine: resultStartLine,
endLine: resultEndLine,
},
};
}
@@ -364,6 +466,158 @@ export class FilesystemTool extends BaseToolWrapper {
};
}
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,
},
};
}
default:
return {
success: false,
@@ -380,8 +634,12 @@ export class FilesystemTool extends BaseToolWrapper {
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'read':
return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
case 'read': {
const lineRange = params.startLine || params.endLine
? ` lines ${params.startLine || 1}-${params.endLine || 'end'}`
: '';
return `Read file "${params.path}"${lineRange}`;
}
case 'write': {
const content = params.content as string;
@@ -416,6 +674,12 @@ export class FilesystemTool extends BaseToolWrapper {
case 'mkdir':
return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
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}"` : ''}`;
default:
return `Unknown action: ${action}`;
}