Skip to content

Commit 5b5e24c

Browse files
Merge pull request #213 from lamemind/main
[FileSystem] directory_tree base implementation
2 parents 0968e43 + 21e07b6 commit 5b5e24c

File tree

1 file changed

+72
-16
lines changed

1 file changed

+72
-16
lines changed

src/filesystem/index.ts

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function expandHome(filepath: string): string {
3535
}
3636

3737
// Store allowed directories in normalized form
38-
const allowedDirectories = args.map(dir =>
38+
const allowedDirectories = args.map(dir =>
3939
normalizePath(path.resolve(expandHome(dir)))
4040
);
4141

@@ -59,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
5959
const absolute = path.isAbsolute(expandedPath)
6060
? path.resolve(expandedPath)
6161
: path.resolve(process.cwd(), expandedPath);
62-
62+
6363
const normalizedRequested = normalizePath(absolute);
6464

6565
// Check if path is within allowed directories
@@ -127,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
127127
path: z.string(),
128128
});
129129

130+
const DirectoryTreeArgsSchema = z.object({
131+
path: z.string(),
132+
});
133+
130134
const MoveFileArgsSchema = z.object({
131135
source: z.string(),
132136
destination: z.string(),
@@ -237,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
237241
// Ensure consistent line endings for diff
238242
const normalizedOriginal = normalizeLineEndings(originalContent);
239243
const normalizedNew = normalizeLineEndings(newContent);
240-
244+
241245
return createTwoFilesPatch(
242246
filepath,
243247
filepath,
@@ -255,33 +259,33 @@ async function applyFileEdits(
255259
): Promise<string> {
256260
// Read file content and normalize line endings
257261
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
258-
262+
259263
// Apply edits sequentially
260264
let modifiedContent = content;
261265
for (const edit of edits) {
262266
const normalizedOld = normalizeLineEndings(edit.oldText);
263267
const normalizedNew = normalizeLineEndings(edit.newText);
264-
268+
265269
// If exact match exists, use it
266270
if (modifiedContent.includes(normalizedOld)) {
267271
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
268272
continue;
269273
}
270-
274+
271275
// Otherwise, try line-by-line matching with flexibility for whitespace
272276
const oldLines = normalizedOld.split('\n');
273277
const contentLines = modifiedContent.split('\n');
274278
let matchFound = false;
275-
279+
276280
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
277281
const potentialMatch = contentLines.slice(i, i + oldLines.length);
278-
282+
279283
// Compare lines with normalized whitespace
280284
const isMatch = oldLines.every((oldLine, j) => {
281285
const contentLine = potentialMatch[j];
282286
return oldLine.trim() === contentLine.trim();
283287
});
284-
288+
285289
if (isMatch) {
286290
// Preserve original indentation of first line
287291
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
@@ -296,33 +300,33 @@ async function applyFileEdits(
296300
}
297301
return line;
298302
});
299-
303+
300304
contentLines.splice(i, oldLines.length, ...newLines);
301305
modifiedContent = contentLines.join('\n');
302306
matchFound = true;
303307
break;
304308
}
305309
}
306-
310+
307311
if (!matchFound) {
308312
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
309313
}
310314
}
311-
315+
312316
// Create unified diff
313317
const diff = createUnifiedDiff(content, modifiedContent, filePath);
314-
318+
315319
// Format diff with appropriate number of backticks
316320
let numBackticks = 3;
317321
while (diff.includes('`'.repeat(numBackticks))) {
318322
numBackticks++;
319323
}
320324
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
321-
325+
322326
if (!dryRun) {
323327
await fs.writeFile(filePath, modifiedContent, 'utf-8');
324328
}
325-
329+
326330
return formattedDiff;
327331
}
328332

@@ -383,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
383387
"finding specific files within a directory. Only works within allowed directories.",
384388
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
385389
},
390+
{
391+
name: "directory_tree",
392+
description:
393+
"Get a recursive tree view of files and directories as a JSON structure. " +
394+
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
395+
"Files have no children array, while directories always have a children array (which may be empty). " +
396+
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
397+
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
398+
},
386399
{
387400
name: "move_file",
388401
description:
@@ -413,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
413426
},
414427
{
415428
name: "list_allowed_directories",
416-
description:
429+
description:
417430
"Returns the list of directories that this server is allowed to access. " +
418431
"Use this to understand which directories are available before trying to access files.",
419432
inputSchema: {
@@ -517,6 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
517530
};
518531
}
519532

533+
case "directory_tree": {
534+
const parsed = DirectoryTreeArgsSchema.safeParse(args);
535+
if (!parsed.success) {
536+
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
537+
}
538+
539+
interface TreeEntry {
540+
name: string;
541+
type: 'file' | 'directory';
542+
children?: TreeEntry[];
543+
}
544+
545+
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
546+
const validPath = await validatePath(currentPath);
547+
const entries = await fs.readdir(validPath, {withFileTypes: true});
548+
const result: TreeEntry[] = [];
549+
550+
for (const entry of entries) {
551+
const entryData: TreeEntry = {
552+
name: entry.name,
553+
type: entry.isDirectory() ? 'directory' : 'file'
554+
};
555+
556+
if (entry.isDirectory()) {
557+
const subPath = path.join(currentPath, entry.name);
558+
entryData.children = await buildTree(subPath);
559+
}
560+
561+
result.push(entryData);
562+
}
563+
564+
return result;
565+
}
566+
567+
const treeData = await buildTree(parsed.data.path);
568+
return {
569+
content: [{
570+
type: "text",
571+
text: JSON.stringify(treeData, null, 2)
572+
}],
573+
};
574+
}
575+
520576
case "move_file": {
521577
const parsed = MoveFileArgsSchema.safeParse(args);
522578
if (!parsed.success) {

0 commit comments

Comments
 (0)