UNPKG

langterm

Version:

Secure CLI tool that translates natural language to shell commands using local AI models via Ollama, with project memory system, reusable command templates (hooks), MCP (Model Context Protocol) support, and dangerous command detection

353 lines (299 loc) 9.28 kB
/** * Simple Location-Based Memory System * * Allows users to save location-specific information as plain text and automatically * enhances commands with contextual data based on the current working directory. * Memory is stored as .langterm-memory.txt in any directory where the user * chooses to save information using the --remember flag. */ import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import chalk from 'chalk'; /** * Memory file name in project directories */ export const MEMORY_FILE = '.langterm-memory.txt'; /** * Maximum memory file size (10KB) */ export const MAX_MEMORY_SIZE = 10 * 1024; /** * Manages location-specific memory and context */ export class MemoryManager { constructor() { this.currentMemory = null; this.currentLocation = null; } /** * Get current working directory */ getCurrentLocation() { return process.cwd(); } /** * Get memory file path for a given location */ getMemoryPath(location = null) { const targetLocation = location || this.getCurrentLocation(); return path.join(targetLocation, MEMORY_FILE); } /** * Check if memory exists for current or specified location */ async hasMemory(location = null) { try { const memoryPath = this.getMemoryPath(location); await fs.access(memoryPath); return true; } catch (error) { return false; } } /** * Load memory for current or specified location */ async loadMemory(location = null) { const targetLocation = location || this.getCurrentLocation(); const memoryPath = this.getMemoryPath(targetLocation); try { const memoryContent = await fs.readFile(memoryPath, 'utf8'); // Simple text-based memory - just return the content as context const memory = { location: targetLocation, context: memoryContent.trim(), created: null, // We don't track creation time in simple format lastUpdated: null }; // Cache current memory this.currentMemory = memory; this.currentLocation = targetLocation; return memory; } catch (error) { if (error.code === 'ENOENT') { return null; // No memory file exists } throw new Error(`Failed to load memory: ${error.message}`); } } /** * Save memory for current or specified location */ async saveMemory(memoryData, location = null) { const targetLocation = location || this.getCurrentLocation(); const memoryPath = this.getMemoryPath(targetLocation); // Extract just the context text from the memory data let contextText = ''; if (typeof memoryData === 'string') { contextText = memoryData; } else if (memoryData && memoryData.context) { contextText = memoryData.context; } else if (memoryData && typeof memoryData === 'object') { // Convert object to simple text representation contextText = Object.entries(memoryData) .map(([key, value]) => `${key}: ${value}`) .join('\n'); } contextText = contextText.trim(); if (!contextText) { throw new Error('No content to save'); } // Check file size if (contextText.length > MAX_MEMORY_SIZE) { throw new Error(`Memory data too large. Maximum size is ${MAX_MEMORY_SIZE} bytes.`); } // Validate for dangerous content this.validateMemoryContent(contextText); try { await fs.writeFile(memoryPath, contextText, 'utf8'); // Create simple memory object for cache const memory = { location: targetLocation, context: contextText, created: null, lastUpdated: null }; // Update cache this.currentMemory = memory; this.currentLocation = targetLocation; return memory; } catch (error) { throw new Error(`Failed to save memory: ${error.message}`); } } /** * Delete memory for current or specified location */ async deleteMemory(location = null) { const targetLocation = location || this.getCurrentLocation(); const memoryPath = this.getMemoryPath(targetLocation); try { await fs.unlink(memoryPath); // Clear cache if deleting current location if (targetLocation === this.currentLocation) { this.currentMemory = null; this.currentLocation = null; } return true; } catch (error) { if (error.code === 'ENOENT') { return false; // File doesn't exist } throw new Error(`Failed to delete memory: ${error.message}`); } } /** * Get memory for current location (cached if available) */ async getCurrentMemory() { const currentLocation = this.getCurrentLocation(); // Return cached memory if it's for current location if (this.currentMemory && this.currentLocation === currentLocation) { return this.currentMemory; } // Load fresh memory return await this.loadMemory(currentLocation); } /** * Add information to current memory */ async addToMemory(data) { const currentMemory = await this.getCurrentMemory(); // Extract text content to add let newText = ''; if (typeof data === 'string') { newText = data; } else if (data && data.context) { newText = data.context; } else if (data && typeof data === 'object') { newText = Object.entries(data) .map(([key, value]) => `${key}: ${value}`) .join('\n'); } let combinedContent = ''; if (currentMemory && currentMemory.context) { combinedContent = currentMemory.context + '\n' + newText; } else { combinedContent = newText; } return await this.saveMemory(combinedContent); } /** * Record a successful command for pattern learning * Note: In simple txt format, we don't track command patterns */ async recordSuccessfulCommand(userInput, generatedCommand) { // Simple text format doesn't track command patterns // This is a no-op for the simplified system return; } /** * Get enhanced context for command generation */ async getEnhancedContext(userInput) { const memory = await this.getCurrentMemory(); if (!memory || !memory.context) return ''; // Return the simple text context from the .txt file return `Location Context: ${memory.context}`; } /** * Find commands similar to user input * Note: Not used in simple txt format */ findRelevantCommands(userInput, commands) { // Simple text format doesn't track command patterns return []; } /** * Validate memory content for dangerous patterns */ validateMemoryContent(content) { if (!content || typeof content !== 'string') { throw new Error('Invalid memory content'); } // Check for dangerous content const dangerousPatterns = [ /rm\s+-rf\s+\//, />\s*\/etc\/passwd/, /format\s+[cC]:/, /__proto__/, /constructor/, /prototype/ ]; for (const pattern of dangerousPatterns) { if (pattern.test(content)) { throw new Error('Memory contains potentially dangerous content'); } } return true; } /** * Validate memory object structure * Note: For simple txt format, this just validates the context string */ validateMemory(memory) { if (!memory || typeof memory !== 'object') { throw new Error('Invalid memory format'); } if (memory.context) { return this.validateMemoryContent(memory.context); } return true; } /** * Get memory statistics across all known locations */ async getMemoryStats() { const current = await this.getCurrentMemory(); return { currentLocation: this.getCurrentLocation(), hasCurrentMemory: !!current, currentMemorySize: current ? current.context.length : 0, lastUpdated: null // Simple format doesn't track timestamps }; } /** * Export memory data for backup */ async exportMemory(location = null) { const memory = await this.loadMemory(location); if (!memory) { throw new Error('No memory found for location'); } // For simple txt format, just return the content return memory.context; } /** * Import memory data from backup */ async importMemory(exportData, location = null) { // For simple txt format, exportData should be a string if (typeof exportData !== 'string') { throw new Error('Invalid export data format for simple text memory'); } return await this.saveMemory(exportData, location); } } /** * Global memory manager instance */ export const memoryManager = new MemoryManager(); /** * Utility function to check if current directory has memory */ export async function hasLocationMemory() { return await memoryManager.hasMemory(); } /** * Utility function to get current location memory */ export async function getLocationMemory() { return await memoryManager.getCurrentMemory(); } /** * Utility function to save location memory */ export async function saveLocationMemory(data) { return await memoryManager.addToMemory(data); }