directory_tree outputs JSON

This commit is contained in:
lamemind 2024-12-09 21:06:10 +01:00
commit 188cd7653c

View file

@ -34,7 +34,7 @@ function expandHome(filepath: string): string {
} }
// Store allowed directories in normalized form // Store allowed directories in normalized form
const allowedDirectories = args.map(dir => const allowedDirectories = args.map(dir =>
normalizePath(path.resolve(expandHome(dir))) normalizePath(path.resolve(expandHome(dir)))
); );
@ -58,7 +58,7 @@ async function validatePath(requestedPath: string): Promise<string> {
const absolute = path.isAbsolute(expandedPath) const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath) ? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath); : path.resolve(process.cwd(), expandedPath);
const normalizedRequested = normalizePath(absolute); const normalizedRequested = normalizePath(absolute);
// Check if path is within allowed directories // Check if path is within allowed directories
@ -195,7 +195,7 @@ async function searchFiles(
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name); const fullPath = path.join(currentPath, entry.name);
try { try {
// Validate each path before processing // Validate each path before processing
await validatePath(fullPath); await validatePath(fullPath);
@ -227,7 +227,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
// Ensure consistent line endings for diff // Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent); const normalizedOriginal = normalizeLineEndings(originalContent);
const normalizedNew = normalizeLineEndings(newContent); const normalizedNew = normalizeLineEndings(newContent);
return createTwoFilesPatch( return createTwoFilesPatch(
filepath, filepath,
filepath, filepath,
@ -245,33 +245,33 @@ async function applyFileEdits(
): Promise<string> { ): Promise<string> {
// Read file content and normalize line endings // Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
// Apply edits sequentially // Apply edits sequentially
let modifiedContent = content; let modifiedContent = content;
for (const edit of edits) { for (const edit of edits) {
const normalizedOld = normalizeLineEndings(edit.oldText); const normalizedOld = normalizeLineEndings(edit.oldText);
const normalizedNew = normalizeLineEndings(edit.newText); const normalizedNew = normalizeLineEndings(edit.newText);
// If exact match exists, use it // If exact match exists, use it
if (modifiedContent.includes(normalizedOld)) { if (modifiedContent.includes(normalizedOld)) {
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
continue; continue;
} }
// Otherwise, try line-by-line matching with flexibility for whitespace // Otherwise, try line-by-line matching with flexibility for whitespace
const oldLines = normalizedOld.split('\n'); const oldLines = normalizedOld.split('\n');
const contentLines = modifiedContent.split('\n'); const contentLines = modifiedContent.split('\n');
let matchFound = false; let matchFound = false;
for (let i = 0; i <= contentLines.length - oldLines.length; i++) { for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
const potentialMatch = contentLines.slice(i, i + oldLines.length); const potentialMatch = contentLines.slice(i, i + oldLines.length);
// Compare lines with normalized whitespace // Compare lines with normalized whitespace
const isMatch = oldLines.every((oldLine, j) => { const isMatch = oldLines.every((oldLine, j) => {
const contentLine = potentialMatch[j]; const contentLine = potentialMatch[j];
return oldLine.trim() === contentLine.trim(); return oldLine.trim() === contentLine.trim();
}); });
if (isMatch) { if (isMatch) {
// Preserve original indentation of first line // Preserve original indentation of first line
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
@ -286,33 +286,33 @@ async function applyFileEdits(
} }
return line; return line;
}); });
contentLines.splice(i, oldLines.length, ...newLines); contentLines.splice(i, oldLines.length, ...newLines);
modifiedContent = contentLines.join('\n'); modifiedContent = contentLines.join('\n');
matchFound = true; matchFound = true;
break; break;
} }
} }
if (!matchFound) { if (!matchFound) {
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
} }
} }
// Create unified diff // Create unified diff
const diff = createUnifiedDiff(content, modifiedContent, filePath); const diff = createUnifiedDiff(content, modifiedContent, filePath);
// Format diff with appropriate number of backticks // Format diff with appropriate number of backticks
let numBackticks = 3; let numBackticks = 3;
while (diff.includes('`'.repeat(numBackticks))) { while (diff.includes('`'.repeat(numBackticks))) {
numBackticks++; numBackticks++;
} }
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
if (!dryRun) { if (!dryRun) {
await fs.writeFile(filePath, modifiedContent, 'utf-8'); await fs.writeFile(filePath, modifiedContent, 'utf-8');
} }
return formattedDiff; return formattedDiff;
} }
@ -376,11 +376,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
{ {
name: "directory_tree", name: "directory_tree",
description: description:
"Get a recursive tree view of files and directories starting from a specified path. " + "Get a recursive tree view of files and directories as a JSON structure. " +
"Results are formatted in a hierarchical ASCII tree structure with proper indentation " + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
"using pipes and dashes (│ ├ └ ─). Files and directories are distinguished " + "Files have no children array, while directories always have a children array (which may be empty). " +
"with [F] and [D] prefixes. This tool provides a comprehensive visualization of nested " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
"directory structures. Only works within allowed directories.",
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
}, },
{ {
@ -413,7 +412,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
}, },
{ {
name: "list_allowed_directories", name: "list_allowed_directories",
description: description:
"Returns the list of directories that this server is allowed to access. " + "Returns the list of directories that this server is allowed to access. " +
"Use this to understand which directories are available before trying to access files.", "Use this to understand which directories are available before trying to access files.",
inputSchema: { inputSchema: {
@ -518,36 +517,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
} }
case "directory_tree": { case "directory_tree": {
const parsed = ListDirectoryArgsSchema.safeParse(args); const parsed = DirectoryTreeArgsSchema.safeParse(args);
if (!parsed.success) { if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
} }
async function buildTree(currentPath: string, prefix = ""): Promise<string> { interface TreeEntry {
name: string;
type: 'file' | 'directory';
children?: TreeEntry[];
}
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
const validPath = await validatePath(currentPath); const validPath = await validatePath(currentPath);
const entries = await fs.readdir(validPath, {withFileTypes: true}); const entries = await fs.readdir(validPath, {withFileTypes: true});
let result = ""; const result: TreeEntry[] = [];
for (let i = 0; i < entries.length; i++) { for (const entry of entries) {
const entry = entries[i]; const entryData: TreeEntry = {
const isLast = i === entries.length - 1; name: entry.name,
const connector = isLast ? "└── " : "├── "; type: entry.isDirectory() ? 'directory' : 'file'
const newPrefix = prefix + (isLast ? " " : "│ "); };
result += `${prefix}${connector}${entry.isDirectory() ? "[D]" : "[F]"} ${entry.name}\n`;
if (entry.isDirectory()) { if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name); const subPath = path.join(currentPath, entry.name);
result += await buildTree(subPath, newPrefix); entryData.children = await buildTree(subPath);
} }
result.push(entryData);
} }
return result; return result;
} }
const treeOutput = await buildTree(parsed.data.path); const treeData = await buildTree(parsed.data.path);
return { return {
content: [{type: "text", text: treeOutput}], content: [{
type: "text",
text: JSON.stringify(treeData, null, 2)
}],
}; };
} }
@ -592,9 +600,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
case "list_allowed_directories": { case "list_allowed_directories": {
return { return {
content: [{ content: [{
type: "text", type: "text",
text: `Allowed directories:\n${allowedDirectories.join('\n')}` text: `Allowed directories:\n${allowedDirectories.join('\n')}`
}], }],
}; };
} }
@ -622,4 +630,4 @@ async function runServer() {
runServer().catch((error) => { runServer().catch((error) => {
console.error("Fatal error running server:", error); console.error("Fatal error running server:", error);
process.exit(1); process.exit(1);
}); });