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
JavaScript
/**
* 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);
}