Skip to main content
Glama
fileSystemToolImpl.ts8.1 kB
import { Repository, FileInfo } from './repository'; import pathLib from 'path'; import fs from 'fs'; // Interface defined in llmService.ts - ensure it matches or import if possible // For now, duplicating for clarity if not directly importable due to module structure interface FileSystemToolService { readFile(path: string): Promise<{ success: boolean; content?: string; error?: string }>; writeFile(path: string, content: string): Promise<{ success: boolean; error?: string }>; editFile(path: string, edits: any): Promise<{ success: boolean; error?: string }>; createDirectory(path: string): Promise<{ success: boolean; error?: string }>; listDirectory(path: string): Promise<{ success: boolean; content?: string[]; error?: string }>; getFileTree(path: string): Promise<{ success: boolean; tree?: FileNode; error?: string }>; } interface FileNode { name: string; path: string; type: 'file' | 'directory'; children?: FileNode[]; } const LSPACE_DIR = '.lspace'; const LSPACE_DIR_SLASH = '.lspace/'; const FORBIDDEN_ACCESS_ERROR = 'Access to the .lspace directory is forbidden.'; export class FileSystemToolImpl implements FileSystemToolService { private repository: Repository; constructor(repository: Repository) { this.repository = repository; } private isPathForbidden(relativePath: string): boolean { return relativePath === LSPACE_DIR || relativePath.startsWith(LSPACE_DIR_SLASH); } async readFile(path: string): Promise<{ success: boolean; content?: string; error?: string }> { try { const relativePath = this.getRelativePath(path); if (this.isPathForbidden(relativePath)) { return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } if (!await this.repository.fileExists(relativePath)) { return { success: false, error: `File not found: ${path}` }; } const content = await this.repository.readFile(relativePath); return { success: true, content }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error reading file ${path}:`, e); return { success: false, error: e.message }; } } async writeFile(path: string, content: string): Promise<{ success: boolean; error?: string }> { try { const relativePath = this.getRelativePath(path); if (this.isPathForbidden(relativePath)) { return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } await this.repository.writeFile(relativePath, content); return { success: true }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error writing file ${path}:`, e); return { success: false, error: e.message }; } } // Basic implementation for editFile. Assumes \'edits\' is the new full content for simplicity for now. // A more advanced version would parse specific edit instructions. async editFile(path: string, edits: string): Promise<{ success: boolean; error?: string }> { try { const relativePath = this.getRelativePath(path); if (this.isPathForbidden(relativePath)) { return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } if (!await this.repository.fileExists(relativePath)) { return { success: false, error: `File not found for editing: ${path}` }; } // Simple overwrite for now, effectively same as writeFile if full content is passed await this.repository.writeFile(relativePath, edits); return { success: true }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error editing file ${path}:`, e); return { success: false, error: e.message }; } } async createDirectory(path: string): Promise<{ success: boolean; error?: string }> { try { const relativePath = this.getRelativePath(path); if (this.isPathForbidden(relativePath)) { return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } await this.repository.ensureDirectoryExists(relativePath); return { success: true }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error creating directory ${path}:`, e); return { success: false, error: e.message }; } } async listDirectory(path: string): Promise<{ success: boolean; content?: string[]; error?: string }> { try { const relativePath = this.getRelativePath(path); if (this.isPathForbidden(relativePath)) { // For listDirectory, if they try to list .lspace itself, return empty or error. // If they list a subdirectory of .lspace, that's also forbidden. return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } const filesInfo: FileInfo[] = await this.repository.listFiles(relativePath); const directoryContent = filesInfo.map(f => pathLib.basename(f.path) + (f.type === 'directory' ? '/' : '')); return { success: true, content: directoryContent }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error listing directory ${path}:`, e); return { success: false, error: e.message }; } } async getFileTree(rootPath: string): Promise<{ success: boolean; tree?: FileNode; error?: string }> { try { const relativeRootPath = this.getRelativePath(rootPath); // getFileTree itself can be called on subdirectories. The .lspace exclusion is handled in buildTreeRecursive for the root. // If rootPath itself is .lspace or inside .lspace, that should be an error here. if (this.isPathForbidden(relativeRootPath)){ return { success: false, error: FORBIDDEN_ACCESS_ERROR }; } const fullRootPath = pathLib.resolve(this.repository.path, relativeRootPath); try { const stats = await fs.promises.stat(fullRootPath); if (!stats.isDirectory()) { return { success: false, error: `Path is not a directory: ${rootPath}` }; } } catch (statError) { return { success: false, error: `Path does not exist, is not a directory, or is not accessible: ${rootPath}` }; } const tree = await this.buildTreeRecursive(relativeRootPath); return { success: true, tree }; } catch (e: any) { console.error(`[FileSystemToolImpl] Error getting file tree for ${rootPath}:`, e); return { success: false, error: e.message }; } } private async buildTreeRecursive(currentPathInRepo: string): Promise<FileNode> { const fullAbsolutePath = pathLib.resolve(this.repository.path, currentPathInRepo); const name = pathLib.basename(fullAbsolutePath); const stats = await fs.promises.stat(fullAbsolutePath); const node: FileNode = { name: name, path: currentPathInRepo, type: stats.isDirectory() ? 'directory' : 'file' }; if (stats.isDirectory()) { node.children = []; const childrenFilesInfo: FileInfo[] = await this.repository.listFiles(currentPathInRepo); for (const childInfo of childrenFilesInfo) { // Skip .lspace directory explicitly if currentPathInRepo is the root ('.') // and the child is named '.lspace'. if (currentPathInRepo === '.' && childInfo.path === LSPACE_DIR) { continue; } // Ensure we only process direct children for this node const relativeToCurrent = pathLib.relative(currentPathInRepo, childInfo.path); if (relativeToCurrent && !relativeToCurrent.includes(pathLib.sep) && relativeToCurrent !== '..') { const childNode = await this.buildTreeRecursive(childInfo.path); node.children.push(childNode); } } } return node; } // Helper to ensure paths passed to repository methods are relative to its root // and not accidentally absolute paths from somewhere else. private getRelativePath(filePath: string): string { if (pathLib.isAbsolute(filePath)) { if (!filePath.startsWith(this.repository.path)) { throw new Error(`Absolute path ${filePath} is outside the repository directory ${this.repository.path}`); } return pathLib.relative(this.repository.path, filePath); } return filePath; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Lspace-io/lspace-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server