Skip to main content
Glama
fs.ts11.6 kB
import fs from 'fs/promises'; import { createWriteStream } from 'fs'; import path from 'path'; import { pipeline } from 'stream/promises'; import { Readable } from 'stream'; import { DocSource } from '../types/index.js'; /** * Error thrown when file system operations fail */ export class FileSystemError extends Error { constructor(message: string, public readonly cause?: unknown) { super(message); this.name = 'FileSystemError'; } } /** * Configuration for cache management */ interface CacheConfig { maxSize: number; // Maximum cache size in bytes (default: 100MB) maxAge: number; // Maximum age of cache files in milliseconds (default: 7 days) cleanupInterval: number; // Cleanup interval in milliseconds (default: 1 hour) } /** * Manages file system operations for documentation storage */ export class FileSystemManager { private docsPath: string; private sourcesFile: string; private cacheDir: string; private cacheConfig: CacheConfig; private cleanupTimer?: NodeJS.Timeout; /** * Creates a new FileSystemManager instance * @param basePath - Base path for storing documentation */ constructor( basePath: string, cacheConfig: CacheConfig = { maxSize: 100 * 1024 * 1024, // 100MB maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days cleanupInterval: 60 * 60 * 1000, // 1 hour } ) { // Ensure absolute path and decode URL-encoded characters this.docsPath = decodeURIComponent(path.resolve(basePath)); this.sourcesFile = path.join(this.docsPath, 'sources.json'); this.cacheDir = path.join(this.docsPath, 'cache'); // Log paths for debugging console.error('\nFileSystemManager paths:'); console.error('- Base path:', this.docsPath); console.error('- Sources file:', this.sourcesFile); console.error('- Cache directory:', this.cacheDir); console.error('- Current working directory:', process.cwd()); // Use provided cache configuration this.cacheConfig = cacheConfig; // Start cache cleanup timer this.startCleanupTimer(); } /** * Starts the cache cleanup timer */ private startCleanupTimer(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { this.cleanupCache().catch(console.error); }, this.cacheConfig.cleanupInterval); } /** * Cleans up old cache files */ private async cleanupCache(): Promise<void> { try { const files = await fs.readdir(this.cacheDir); const now = Date.now(); let totalSize = 0; // Get file stats and sort by access time const fileStats = await Promise.all( files.map(async file => { const filePath = path.join(this.cacheDir, file); const stats = await fs.stat(filePath); return { file, path: filePath, stats }; }) ); fileStats.sort((a, b) => a.stats.atime.getTime() - b.stats.atime.getTime()); // Remove old files and check total size for (const { file, path: filePath, stats } of fileStats) { if (stats.mtimeMs < now - this.cacheConfig.maxAge) { await fs.unlink(filePath); continue; } totalSize += stats.size; if (totalSize > this.cacheConfig.maxSize) { await fs.unlink(filePath); } } } catch (error) { console.error('Cache cleanup failed:', error); } } /** * Ensures required directories exist * @throws {FileSystemError} If directory creation fails */ async ensureDirectories(): Promise<void> { try { console.error('\nEnsuring directories:'); console.error('- Creating docs path:', this.docsPath); await fs.mkdir(this.docsPath, { recursive: true }); const docsStats = await fs.stat(this.docsPath); console.error('- Docs path created:', docsStats.isDirectory()); console.error('- Docs path permissions:', docsStats.mode.toString(8)); console.error('- Creating cache dir:', this.cacheDir); await fs.mkdir(this.cacheDir, { recursive: true }); const cacheStats = await fs.stat(this.cacheDir); console.error('- Cache dir created:', cacheStats.isDirectory()); console.error('- Cache dir permissions:', cacheStats.mode.toString(8)); // Try to write a test file const testFile = path.join(this.docsPath, 'test.txt'); console.error('- Testing write permissions:', testFile); try { await fs.writeFile(testFile, 'test'); await fs.unlink(testFile); console.error('- Write test successful'); } catch (error) { // Ignore errors when deleting test file console.error('- Write test completed with cleanup warning:', error); } } catch (error) { console.error('Failed to ensure directories:', error); if (error instanceof Error) { console.error('Error details:', error.message); console.error('Stack trace:', error.stack); } throw new FileSystemError('Failed to create required directories', error); } } /** * Saves documentation content to cache using streams * @param name - Documentation name * @param content - Content to save * @throws {FileSystemError} If save operation fails */ async saveDocumentation(name: string, content: string): Promise<void> { try { const filename = this.getDocumentationFileName(name); await this.ensureDirectories(); // Create a readable stream from the content const readStream = Readable.from(content); // Create a write stream to the file const writeStream = createWriteStream(path.join(this.cacheDir, filename), { flags: 'w' }); // Use pipeline for proper error handling and cleanup await pipeline(readStream, writeStream); } catch (error) { throw new FileSystemError(`Failed to save documentation: ${name}`, error); } } /** * Saves documentation sources metadata * @param docs - Array of documentation sources * @throws {FileSystemError} If save operation fails */ async saveSources(docs: DocSource[]): Promise<void> { try { console.error('\nSaving sources:'); console.error('- Path:', this.sourcesFile); console.error('- Docs count:', docs.length); console.error('- Content:', JSON.stringify(docs, null, 2)); await this.ensureDirectories(); console.error('- Directories ensured'); // Write directly using fs.writeFile await fs.writeFile(this.sourcesFile, JSON.stringify(docs, null, 2), { mode: 0o666 }); console.error('- File written'); // Verify file was created const exists = await fs .access(this.sourcesFile) .then(() => true) .catch(() => false); console.error('- File exists:', exists); if (exists) { const stats = await fs.stat(this.sourcesFile); console.error('- File size:', stats.size, 'bytes'); console.error('- File permissions:', stats.mode.toString(8)); const content = await fs.readFile(this.sourcesFile, 'utf-8'); console.error('- File content:', content); } else { throw new Error('File was not created'); } } catch (error) { console.error('Failed to save sources:', error); console.error('Error details:', error instanceof Error ? error.message : String(error)); console.error('Stack trace:', error instanceof Error ? error.stack : 'No stack trace'); throw new FileSystemError('Failed to save documentation sources', error); } } /** * Loads documentation sources metadata * @returns Array of documentation sources * @throws {FileSystemError} If load operation fails */ async loadSources(): Promise<DocSource[]> { try { console.error('\nLoading sources:'); console.error('- Path:', this.sourcesFile); await this.ensureDirectories(); console.error('- Directories ensured'); const exists = await fs .access(this.sourcesFile) .then(() => true) .catch(() => false); console.error('- File exists:', exists); if (!exists) { console.error('- No sources file found, returning empty array'); return []; } const content = await fs.readFile(this.sourcesFile, 'utf-8'); console.error('- File content length:', content.length); const docs = JSON.parse(content); console.error('- Parsed docs count:', docs.length); return docs; } catch (error) { console.error('Error loading sources:', error); console.error('Error details:', error instanceof Error ? error.message : String(error)); return []; } } /** * Searches for text in cached documentation using streams * @param name - Documentation name * @param searchQuery - Text to search for * @returns Whether the text was found */ async searchInDocumentation(name: string, searchQuery: string): Promise<boolean> { try { const filename = this.getDocumentationFileName(name); const filePath = path.join(this.cacheDir, filename); const content = await fs.readFile(filePath, 'utf-8'); const chunks = content.match(/.{1,1048576}/g) || []; // Split into 1MB chunks for (const chunk of chunks) { if (chunk.toLowerCase().includes(searchQuery.toLowerCase())) { return true; } } return false; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return false; } throw new FileSystemError(`Failed to search documentation: ${name}`, error); } } /** * Lists all cached documentation files * @returns Array of filenames * @throws {FileSystemError} If directory reading fails */ async listDocumentationFiles(): Promise<string[]> { try { await this.ensureDirectories(); return fs.readdir(this.cacheDir); } catch (error) { throw new FileSystemError('Failed to list documentation files', error); } } /** * Gets documentation file path * @param name - Documentation name * @returns Path to documentation file */ getDocumentationPath(name: string): string { return path.join(this.cacheDir, this.getDocumentationFileName(name)); } /** * Checks if documentation exists in cache * @param name - Documentation name * @returns Whether documentation exists */ async hasDocumentation(name: string): Promise<boolean> { try { await fs.access(this.getDocumentationPath(name)); return true; } catch { return false; } } /** * Gets documentation file name * @param name - Documentation name * @returns Sanitized file name */ private getDocumentationFileName(name: string): string { return `${name.toLowerCase().replace(/\s+/g, '_')}.html`; } /** * Gets base documentation path * @returns Base path for documentation storage */ getDocsPath(): string { return this.docsPath; } /** * Gets cache directory path * @returns Path to cache directory */ getCachePath(): string { return this.cacheDir; } /** * Updates cache configuration * @param config - New cache configuration */ updateCacheConfig(config: Partial<CacheConfig>): void { this.cacheConfig = { ...this.cacheConfig, ...config }; this.startCleanupTimer(); // Restart timer with new interval } /** * Cleanup resources */ destroy(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } } }

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/aindreyway/mcp-codex-keeper'

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