Skip to content

Commit 188cd76

Browse files
committed
directory_tree outputs JSON
1 parent a6dfe1e commit 188cd76

File tree

1 file changed

+47
-39
lines changed

1 file changed

+47
-39
lines changed

src/filesystem/index.ts

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function expandHome(filepath: string): string {
3434
}
3535

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

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

6464
// Check if path is within allowed directories
@@ -195,7 +195,7 @@ async function searchFiles(
195195

196196
for (const entry of entries) {
197197
const fullPath = path.join(currentPath, entry.name);
198-
198+
199199
try {
200200
// Validate each path before processing
201201
await validatePath(fullPath);
@@ -227,7 +227,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
227227
// Ensure consistent line endings for diff
228228
const normalizedOriginal = normalizeLineEndings(originalContent);
229229
const normalizedNew = normalizeLineEndings(newContent);
230-
230+
231231
return createTwoFilesPatch(
232232
filepath,
233233
filepath,
@@ -245,33 +245,33 @@ async function applyFileEdits(
245245
): Promise<string> {
246246
// Read file content and normalize line endings
247247
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
248-
248+
249249
// Apply edits sequentially
250250
let modifiedContent = content;
251251
for (const edit of edits) {
252252
const normalizedOld = normalizeLineEndings(edit.oldText);
253253
const normalizedNew = normalizeLineEndings(edit.newText);
254-
254+
255255
// If exact match exists, use it
256256
if (modifiedContent.includes(normalizedOld)) {
257257
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
258258
continue;
259259
}
260-
260+
261261
// Otherwise, try line-by-line matching with flexibility for whitespace
262262
const oldLines = normalizedOld.split('\n');
263263
const contentLines = modifiedContent.split('\n');
264264
let matchFound = false;
265-
265+
266266
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
267267
const potentialMatch = contentLines.slice(i, i + oldLines.length);
268-
268+
269269
// Compare lines with normalized whitespace
270270
const isMatch = oldLines.every((oldLine, j) => {
271271
const contentLine = potentialMatch[j];
272272
return oldLine.trim() === contentLine.trim();
273273
});
274-
274+
275275
if (isMatch) {
276276
// Preserve original indentation of first line
277277
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
@@ -286,33 +286,33 @@ async function applyFileEdits(
286286
}
287287
return line;
288288
});
289-
289+
290290
contentLines.splice(i, oldLines.length, ...newLines);
291291
modifiedContent = contentLines.join('\n');
292292
matchFound = true;
293293
break;
294294
}
295295
}
296-
296+
297297
if (!matchFound) {
298298
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
299299
}
300300
}
301-
301+
302302
// Create unified diff
303303
const diff = createUnifiedDiff(content, modifiedContent, filePath);
304-
304+
305305
// Format diff with appropriate number of backticks
306306
let numBackticks = 3;
307307
while (diff.includes('`'.repeat(numBackticks))) {
308308
numBackticks++;
309309
}
310310
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
311-
311+
312312
if (!dryRun) {
313313
await fs.writeFile(filePath, modifiedContent, 'utf-8');
314314
}
315-
315+
316316
return formattedDiff;
317317
}
318318

@@ -376,11 +376,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
376376
{
377377
name: "directory_tree",
378378
description:
379-
"Get a recursive tree view of files and directories starting from a specified path. " +
380-
"Results are formatted in a hierarchical ASCII tree structure with proper indentation " +
381-
"using pipes and dashes (│ ├ └ ─). Files and directories are distinguished " +
382-
"with [F] and [D] prefixes. This tool provides a comprehensive visualization of nested " +
383-
"directory structures. Only works within allowed directories.",
379+
"Get a recursive tree view of files and directories as a JSON structure. " +
380+
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
381+
"Files have no children array, while directories always have a children array (which may be empty). " +
382+
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
384383
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
385384
},
386385
{
@@ -413,7 +412,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
413412
},
414413
{
415414
name: "list_allowed_directories",
416-
description:
415+
description:
417416
"Returns the list of directories that this server is allowed to access. " +
418417
"Use this to understand which directories are available before trying to access files.",
419418
inputSchema: {
@@ -518,36 +517,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
518517
}
519518

520519
case "directory_tree": {
521-
const parsed = ListDirectoryArgsSchema.safeParse(args);
520+
const parsed = DirectoryTreeArgsSchema.safeParse(args);
522521
if (!parsed.success) {
523522
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
524523
}
525524

526-
async function buildTree(currentPath: string, prefix = ""): Promise<string> {
525+
interface TreeEntry {
526+
name: string;
527+
type: 'file' | 'directory';
528+
children?: TreeEntry[];
529+
}
530+
531+
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
527532
const validPath = await validatePath(currentPath);
528533
const entries = await fs.readdir(validPath, {withFileTypes: true});
529-
let result = "";
530-
531-
for (let i = 0; i < entries.length; i++) {
532-
const entry = entries[i];
533-
const isLast = i === entries.length - 1;
534-
const connector = isLast ? "└── " : "├── ";
535-
const newPrefix = prefix + (isLast ? " " : "│ ");
534+
const result: TreeEntry[] = [];
536535

537-
result += `${prefix}${connector}${entry.isDirectory() ? "[D]" : "[F]"} ${entry.name}\n`;
536+
for (const entry of entries) {
537+
const entryData: TreeEntry = {
538+
name: entry.name,
539+
type: entry.isDirectory() ? 'directory' : 'file'
540+
};
538541

539542
if (entry.isDirectory()) {
540543
const subPath = path.join(currentPath, entry.name);
541-
result += await buildTree(subPath, newPrefix);
544+
entryData.children = await buildTree(subPath);
542545
}
546+
547+
result.push(entryData);
543548
}
544549

545550
return result;
546551
}
547552

548-
const treeOutput = await buildTree(parsed.data.path);
553+
const treeData = await buildTree(parsed.data.path);
549554
return {
550-
content: [{type: "text", text: treeOutput}],
555+
content: [{
556+
type: "text",
557+
text: JSON.stringify(treeData, null, 2)
558+
}],
551559
};
552560
}
553561

@@ -592,9 +600,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
592600

593601
case "list_allowed_directories": {
594602
return {
595-
content: [{
596-
type: "text",
597-
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
603+
content: [{
604+
type: "text",
605+
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
598606
}],
599607
};
600608
}
@@ -622,4 +630,4 @@ async function runServer() {
622630
runServer().catch((error) => {
623631
console.error("Fatal error running server:", error);
624632
process.exit(1);
625-
});
633+
});

0 commit comments

Comments
 (0)