FileScopeMCP

by admica
Verified
import * as fs from 'fs/promises'; import * as path from 'path'; import * as fsSync from 'fs'; import { FileNode, FileTreeConfig, FileTreeStorage } from './types.js'; import { getProjectRoot } from './global-state.js'; // Keep a map of all loaded file trees const loadedTrees = new Map<string, FileTreeStorage>(); /** * Normalizes paths to use forward slashes and handles URL encoding * Works with both relative and absolute paths on any platform * @param inputPath The path to normalize * @param baseDirectory Optional base directory to resolve relative paths against (defaults to project root) */ export function normalizeAndResolvePath(inputPath: string, baseDirectory?: string): string { try { // Handle special case for current directory if (inputPath === '.' || inputPath === './') { return getProjectRoot().replace(/\\/g, '/').replace(/\/+/g, '/'); } // Decode URL encoding if present const decoded = inputPath.includes('%') ? decodeURIComponent(inputPath) : inputPath; // Handle Windows paths with drive letters that may start with a slash const cleanPath = decoded.match(/^\/[a-zA-Z]:/) ? decoded.substring(1) : decoded; // If it's already an absolute path, normalize it directly if (path.isAbsolute(cleanPath)) { return cleanPath.replace(/\\/g, '/').replace(/\/+/g, '/'); } // For relative paths, resolve against the base directory const base = baseDirectory || getProjectRoot(); console.error(`Resolving relative path ${cleanPath} against base ${base}`); const fullPath = path.resolve(base, cleanPath); // Normalize to forward slashes for consistency and remove duplicate slashes return fullPath.replace(/\\/g, '/').replace(/\/+/g, '/'); } catch (error) { console.error(`Failed to normalize path: ${inputPath}`, error); // Return the input as fallback return inputPath; } } /** * Ensures a directory exists, creating it if necessary */ async function ensureDirectoryExists(dirPath: string): Promise<void> { try { await fs.mkdir(dirPath, { recursive: true }); } catch (error) { // EEXIST is fine - directory already exists if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { throw error; } } } /** * Creates a new file tree configuration */ export async function createFileTreeConfig(filename: string, baseDirectory: string): Promise<FileTreeConfig> { console.error('Creating file tree config...'); console.error('Input filename:', filename); console.error('Input baseDirectory:', baseDirectory); // Handle special case for current directory if (baseDirectory === '.' || baseDirectory === './') { baseDirectory = getProjectRoot(); console.error('Resolved "." to project root:', baseDirectory); } // Normalize paths const normalizedBase = normalizeAndResolvePath(baseDirectory); console.error('Normalized base directory:', normalizedBase); // For the filename, we only want the basename, not the full path const basename = path.basename(filename); const cleanFilename = basename.endsWith('.json') ? basename : `${basename}.json`; console.error('Clean filename:', cleanFilename); // Ensure the base directory exists console.error('Creating base directory if needed:', normalizedBase); await ensureDirectoryExists(normalizedBase); const config = { filename: cleanFilename, baseDirectory: normalizedBase, projectRoot: getProjectRoot(), // Always use the global project root lastUpdated: new Date() }; console.error('Created config:', config); return config; } /** * Saves a file tree to disk */ export async function saveFileTree(config: FileTreeConfig, fileTree: FileNode): Promise<void> { try { console.error('Save file tree called with config:', JSON.stringify(config, null, 2)); // Save in the current working directory const filePath = path.join(process.cwd(), config.filename); console.error('Current working directory:', process.cwd()); console.error('Filename:', config.filename); console.error('Final path:', filePath); const data = { config: { ...config, lastUpdated: new Date() }, fileTree }; console.error('File tree before saving:', JSON.stringify(fileTree, null, 2)); console.error('Excluded files should not be present in the tree.'); console.error('Writing file...'); fsSync.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); console.error('Successfully saved file tree'); } catch (error) { console.error('Error saving file tree:', error); console.error('Error details:', error instanceof Error ? error.stack : String(error)); throw error; } } /** * Loads a file tree from disk, or returns null if it doesn't exist */ export async function loadFileTree(filename: string): Promise<FileTreeStorage> { try { // Check if we have a cached version const cached = loadedTrees.get(filename); if (cached) { console.error(`Using cached file tree for ${filename}`); return cached; } // Load from file const filePath = path.resolve(process.cwd(), filename); console.error(`Loading file tree from: ${filePath}`); const content = await fs.readFile(filePath, 'utf-8'); const storage: FileTreeStorage = JSON.parse(content); // Update the cache loadedTrees.set(filename, storage); return storage; } catch (error) { console.error(`Failed to load file tree from ${filename}:`, error); throw error; } } /** * Gets a list of all saved file trees */ export async function listSavedFileTrees(): Promise<{type: "text", text: string}[]> { try { const files = await fs.readdir(process.cwd()); const jsonFiles = files.filter(file => file.endsWith('.json')); return jsonFiles.map(file => ({ type: 'text' as const, text: file })); } catch (error) { console.error('Error listing file trees:', error); return []; } } /** * Updates a specific file node in the tree * Returns true if the node was found and updated, false otherwise */ export function updateFileNode(fileTree: FileNode, filePath: string, updates: Partial<FileNode>): boolean { // Normalize the path for consistent comparison const normalizedInputPath = filePath.split(path.sep).join('/'); // Function to recursively find and update the node function findAndUpdate(node: FileNode): boolean { const normalizedNodePath = node.path.split(path.sep).join('/'); // Try exact match if (normalizedNodePath === normalizedInputPath) { // Found the node, apply updates Object.assign(node, updates); return true; } // Try case-insensitive match for Windows compatibility if (normalizedNodePath.toLowerCase() === normalizedInputPath.toLowerCase()) { // Found the node, apply updates Object.assign(node, updates); return true; } // Check if the path ends with our target (to handle relative vs absolute paths) if (normalizedInputPath.endsWith(normalizedNodePath) || normalizedNodePath.endsWith(normalizedInputPath)) { // Found the node, apply updates Object.assign(node, updates); return true; } // Check children if this is a directory if (node.isDirectory && node.children) { for (const child of node.children) { if (findAndUpdate(child)) { return true; } } } return false; } return findAndUpdate(fileTree); } /** * Retrieves a specific file node from the tree */ export function getFileNode(fileTree: FileNode, filePath: string): FileNode | null { // Normalize the path for consistent comparison const normalizedInputPath = filePath.split(path.sep).join('/'); // Function to recursively find the node function findNode(node: FileNode): FileNode | null { const normalizedNodePath = node.path.split(path.sep).join('/'); // Try exact match if (normalizedNodePath === normalizedInputPath) { return node; } // Try case-insensitive match for Windows compatibility if (normalizedNodePath.toLowerCase() === normalizedInputPath.toLowerCase()) { return node; } // Check if the path ends with our target (to handle relative vs absolute paths) if (normalizedInputPath.endsWith(normalizedNodePath) || normalizedNodePath.endsWith(normalizedInputPath)) { return node; } // Check children if this is a directory if (node.isDirectory && node.children) { for (const child of node.children) { const found = findNode(child); if (found) { return found; } } } return null; } return findNode(fileTree); }
ID: mcrren8xsa