#!/usr/bin/env node /** * LibreChat Cache Flush Utility * * This script flushes the cache store used by LibreChat, whether it's * Redis (if configured) or file-based cache. * * Usage: * npm run flush-cache * node config/flush-cache.js * node config/flush-cache.js --help */ const path = require('path'); const fs = require('fs'); // Set up environment require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const { USE_REDIS, REDIS_URI, REDIS_USERNAME, REDIS_PASSWORD, REDIS_CA, REDIS_KEY_PREFIX, USE_REDIS_CLUSTER, REDIS_USE_ALTERNATIVE_DNS_LOOKUP, } = process.env; // Simple utility function const isEnabled = (value) => value === 'true' || value === true; // Helper function to read Redis CA certificate const getRedisCA = () => { if (!REDIS_CA) { return null; } try { if (fs.existsSync(REDIS_CA)) { return fs.readFileSync(REDIS_CA, 'utf8'); } else { console.warn(`⚠️ Redis CA certificate file not found: ${REDIS_CA}`); return null; } } catch (error) { console.error(`❌ Failed to read Redis CA certificate file '${REDIS_CA}':`, error.message); return null; } }; async function showHelp() { console.log(` LibreChat Cache Flush Utility DESCRIPTION: Flushes the cache store used by LibreChat. Automatically detects whether Redis or file-based cache is being used and flushes accordingly. USAGE: npm run flush-cache node config/flush-cache.js [options] OPTIONS: --help, -h Show this help message --dry-run Show what would be flushed without actually doing it --verbose, -v Show detailed output CACHE TYPES: • Redis Cache: Flushes all keys with the configured Redis prefix • File Cache: Removes ./data/logs.json and ./data/violations.json WHAT GETS FLUSHED: • User sessions and authentication tokens • Configuration cache • Model queries cache • Rate limiting data • Conversation titles cache • File upload progress • SharePoint tokens • And more... NOTE: This will log out all users and may require them to re-authenticate. `); } async function flushRedisCache(dryRun = false, verbose = false) { try { console.log('🔍 Redis cache detected'); if (verbose) { console.log(` URI: ${REDIS_URI ? REDIS_URI.replace(/\/\/.*@/, '//***:***@') : 'Not set'}`); console.log(` Prefix: ${REDIS_KEY_PREFIX || 'None'}`); } // Create Redis client using same pattern as main app const IoRedis = require('ioredis'); let redis; // Parse credentials from URI or use environment variables (same as redisClients.ts) const urls = (REDIS_URI || '').split(',').map((uri) => new URL(uri)); const username = urls[0]?.username || REDIS_USERNAME; const password = urls[0]?.password || REDIS_PASSWORD; const ca = getRedisCA(); // Redis options (matching redisClients.ts configuration) const redisOptions = { username: username, password: password, tls: ca ? { ca } : undefined, connectTimeout: 10000, maxRetriesPerRequest: 3, enableOfflineQueue: true, lazyConnect: false, }; // Handle cluster vs single Redis (same logic as redisClients.ts) const useCluster = urls.length > 1 || isEnabled(USE_REDIS_CLUSTER); if (useCluster) { const clusterOptions = { redisOptions, enableOfflineQueue: true, }; // Add DNS lookup for AWS ElastiCache if needed (same as redisClients.ts) if (isEnabled(REDIS_USE_ALTERNATIVE_DNS_LOOKUP)) { clusterOptions.dnsLookup = (address, callback) => callback(null, address); } redis = new IoRedis.Cluster( urls.map((url) => ({ host: url.hostname, port: parseInt(url.port, 10) || 6379 })), clusterOptions, ); } else { // @ts-ignore - ioredis default export is constructable despite linter warning redis = new IoRedis(REDIS_URI, redisOptions); } // Wait for connection await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Connection timeout')); }, 10000); redis.once('ready', () => { clearTimeout(timeout); resolve(undefined); }); redis.once('error', (err) => { clearTimeout(timeout); reject(err); }); }); if (dryRun) { console.log('🔍 [DRY RUN] Would flush Redis cache'); try { let allKeys = []; if (useCluster) { const nodes = redis.nodes('master'); console.log(` Cluster detected: ${nodes.length} master nodes`); for (const node of nodes) { const keys = await node.keys('*'); allKeys = allKeys.concat(keys); } } else { allKeys = await redis.keys('*'); } console.log(` Would delete ${allKeys.length} keys`); if (verbose && allKeys.length > 0) { console.log( ' Sample keys:', allKeys.slice(0, 10).join(', ') + (allKeys.length > 10 ? '...' : ''), ); } } catch (error) { console.log(' Could not fetch keys for preview:', error.message); } await redis.disconnect(); return true; } // Get key count before flushing let keyCount = 0; try { if (useCluster) { const nodes = redis.nodes('master'); for (const node of nodes) { const keys = await node.keys('*'); keyCount += keys.length; } } else { const keys = await redis.keys('*'); keyCount = keys.length; } } catch (_error) { // Continue with flush even if we can't count keys } // Flush the Redis cache if (useCluster) { const nodes = redis.nodes('master'); await Promise.all(nodes.map((node) => node.flushdb())); console.log(`✅ Redis cluster cache flushed successfully (${nodes.length} master nodes)`); } else { await redis.flushdb(); console.log('✅ Redis cache flushed successfully'); } if (keyCount > 0) { console.log(` Deleted ${keyCount} keys`); } await redis.disconnect(); return true; } catch (error) { console.error('❌ Error flushing Redis cache:', error.message); if (verbose) { console.error(' Full error:', error); } return false; } } async function flushFileCache(dryRun = false, verbose = false) { const dataDir = path.join(__dirname, '..', 'data'); const filesToClear = [path.join(dataDir, 'logs.json'), path.join(dataDir, 'violations.json')]; console.log('🔍 Checking file-based cache'); if (dryRun) { console.log('🔍 [DRY RUN] Would flush file cache'); for (const filePath of filesToClear) { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); console.log( ` Would delete: ${path.basename(filePath)} (${(stats.size / 1024).toFixed(1)} KB)`, ); } } return true; } let deletedCount = 0; let totalSize = 0; for (const filePath of filesToClear) { try { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); totalSize += stats.size; fs.unlinkSync(filePath); deletedCount++; if (verbose) { console.log( ` ✅ Deleted ${path.basename(filePath)} (${(stats.size / 1024).toFixed(1)} KB)`, ); } } } catch (error) { if (verbose) { console.log(` ❌ Failed to delete ${path.basename(filePath)}: ${error.message}`); } } } if (deletedCount > 0) { console.log('✅ File cache flushed successfully'); console.log(` Deleted ${deletedCount} cache files (${(totalSize / 1024).toFixed(1)} KB)`); } else { console.log('ℹ️ No file cache to flush'); } return true; } async function restartRecommendation() { console.log('\n💡 RECOMMENDATION:'); console.log(' For complete cache clearing, especially for in-memory caches,'); console.log(' consider restarting the LibreChat backend:'); console.log(''); console.log(' npm run backend:stop'); console.log(' npm run backend:dev'); console.log(''); } async function main() { const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const verbose = args.includes('--verbose') || args.includes('-v'); const help = args.includes('--help') || args.includes('-h'); if (help) { await showHelp(); return; } console.log('🧹 LibreChat Cache Flush Utility'); console.log('================================'); if (dryRun) { console.log('🔍 DRY RUN MODE - No actual changes will be made\n'); } let success = true; const isRedisEnabled = isEnabled(USE_REDIS) || (REDIS_URI != null && REDIS_URI !== ''); // Flush the appropriate cache type if (isRedisEnabled) { success = (await flushRedisCache(dryRun, verbose)) && success; } else { console.log('ℹ️ Redis not configured, using file-based cache only'); } // Always check file cache success = (await flushFileCache(dryRun, verbose)) && success; console.log('\n' + '='.repeat(50)); if (success) { if (dryRun) { console.log('✅ Cache flush preview completed'); console.log(' Run without --dry-run to actually flush the cache'); } else { console.log('✅ Cache flush completed successfully'); console.log('⚠️ Note: All users will need to re-authenticate'); } if (!isRedisEnabled) { await restartRecommendation(); } } else { console.log('❌ Cache flush completed with errors'); console.log(' Check the output above for details'); process.exit(1); } } // Handle errors gracefully process.on('unhandledRejection', (error) => { console.error('❌ Unhandled error:', error); process.exit(1); }); process.on('uncaughtException', (error) => { console.error('❌ Uncaught exception:', error); process.exit(1); }); // Run the main function if (require.main === module) { main().catch((error) => { console.error('❌ Fatal error:', error); process.exit(1); }); } module.exports = { flushRedisCache, flushFileCache };