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