Skip to main content
Glama
filePlaceholderResolver.ts6.95 kB
import { readFileSync, statSync } from 'fs'; import { resolve, relative, isAbsolute } from 'path'; import { FilePlaceholderConfig, FilePlaceholderError, FilePlaceholderErrorCode, FilePlaceholderResolutionResult, DEFAULT_FILE_PLACEHOLDER_CONFIG, FILE_PLACEHOLDER_ERROR_CODES, FILE_PLACEHOLDER_ENV_VARS, } from './filePlaceholderTypes.js'; /** * File Placeholder Resolver * * Resolves {{file:...}} placeholders in objects by reading file contents. * Supports multiple placeholders in single strings and recursive object traversal. */ /** * Load configuration from environment variables */ function loadFilePlaceholderConfig(): Required<FilePlaceholderConfig> { const maxFileSize = process.env[FILE_PLACEHOLDER_ENV_VARS.MAX_FILE_SIZE]; return { maxFileSize: maxFileSize ? parseInt(maxFileSize, 10) : DEFAULT_FILE_PLACEHOLDER_CONFIG.maxFileSize, }; } /** * Validate file path - requires absolute paths only */ function validateFilePath(filePath: string): void { // Require absolute paths only if (!isAbsolute(filePath)) { throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_INVALID_PATH, `File path must be absolute (start with /). Got: ${filePath}. Use full path like /home/user/file.txt`, `{{file:${filePath}}}`, filePath ); } // Check for directory traversal attempts if (filePath.includes('..')) { throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_INVALID_PATH, 'File path contains directory traversal (..) which is not allowed', `{{file:${filePath}}}`, filePath ); } } /** * Read file content with size validation */ function readFileContent(filePath: string, maxFileSize: number): string { try { // Validate path (requires absolute path) validateFilePath(filePath); // Check if file exists and get size const stats = statSync(filePath); if (!stats.isFile()) { throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_NOT_FOUND, 'Path exists but is not a file', `{{file:${filePath}}}`, filePath ); } // Check file size if (stats.size > maxFileSize) { throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_SIZE_EXCEEDED, `File size (${stats.size} bytes) exceeds maximum allowed size (${maxFileSize} bytes)`, `{{file:${filePath}}}`, filePath ); } // Read file content const content = readFileSync(filePath, 'utf-8'); return content; } catch (error) { if (error instanceof FilePlaceholderError) { throw error; } // Handle file not found if ((error as any)?.code === 'ENOENT') { throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_NOT_FOUND, 'File not found', `{{file:${filePath}}}`, filePath, error as Error ); } // Handle other read errors throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_READ_ERROR, `Failed to read file: ${(error as Error).message}`, `{{file:${filePath}}}`, filePath, error as Error ); } } /** * Resolve file placeholders in a string */ function resolveStringPlaceholders( input: string, maxFileSize: number, result: FilePlaceholderResolutionResult ): string { // Regex to match {{file:...}} patterns const placeholderRegex = /\{\{file:([^}]+)\}\}/g; let output = input; let match; while ((match = placeholderRegex.exec(input)) !== null) { const placeholder = match[0]; const filePath = match[1].trim(); try { const fileContent = readFileContent(filePath, maxFileSize); // Replace the placeholder with file content output = output.replace(placeholder, fileContent); // Update result tracking result.resolved = true; result.placeholderCount++; result.totalResolvedSize += fileContent.length; result.resolvedFiles.push(filePath); } catch (error) { if (error instanceof FilePlaceholderError) { throw error; } // Convert unknown errors to FilePlaceholderError throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_ERROR, `Unexpected error resolving placeholder: ${(error as Error).message}`, placeholder, filePath, error as Error ); } } return output; } /** * Recursively resolve file placeholders in any data structure */ function resolveDataStructurePlaceholders( input: any, maxFileSize: number, result: FilePlaceholderResolutionResult ): any { if (typeof input === 'string') { return resolveStringPlaceholders(input, maxFileSize, result); } if (Array.isArray(input)) { return input.map(item => resolveDataStructurePlaceholders(item, maxFileSize, result)); } if (input && typeof input === 'object') { const resolved: any = {}; for (const [key, value] of Object.entries(input)) { resolved[key] = resolveDataStructurePlaceholders(value, maxFileSize, result); } return resolved; } return input; } /** * Main function to resolve file placeholders in any data structure * * @param input - The data structure to process (can be any type) * @param config - Optional configuration (uses environment defaults if not provided) * @returns Object with resolved data and resolution metadata */ export function resolveFilePlaceholders( input: any, config?: Partial<FilePlaceholderConfig> ): { data: any; result: FilePlaceholderResolutionResult } { const finalConfig = { ...DEFAULT_FILE_PLACEHOLDER_CONFIG, ...loadFilePlaceholderConfig(), ...config }; const result: FilePlaceholderResolutionResult = { resolved: false, placeholderCount: 0, totalResolvedSize: 0, resolvedFiles: [], }; try { const resolvedData = resolveDataStructurePlaceholders( input, finalConfig.maxFileSize, result ); return { data: resolvedData, result }; } catch (error) { if (error instanceof FilePlaceholderError) { throw error; } // Convert unknown errors throw new FilePlaceholderError( FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_ERROR, `Failed to resolve file placeholders: ${(error as Error).message}`, 'unknown', 'unknown', error as Error ); } } /** * Check if a string contains file placeholders without resolving them */ export function hasFilePlaceholders(input: string): boolean { return /\{\{file:[^}]+\}\}/.test(input); } /** * Get all file placeholders in a string */ export function extractFilePlaceholders(input: string): string[] { const matches = input.match(/\{\{file:([^}]+)\}\}/g); return matches ? matches.map(match => match.replace(/\{\{file:|\}\}/g, '').trim()) : []; }

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/ClearSkye/SkyeNet-MCP-ACE'

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