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

374 lines (319 loc) • 11.6 kB
#!/usr/bin/env node import chalk from 'chalk'; import { spawn } from 'child_process'; import { loadConfig, saveConfig } from './lib/config.js'; import { checkOllama, getModels, generateCommand } from './lib/ollama.js'; import { checkDangerousCommand, confirmDangerousCommand, confirmWarningCommand, prompt } from './lib/security.js'; import { showHelp, parseArgs, selectModel, setupMCPServers, showMCPStatus, createHook, showHooksList, editHook, deleteHook, searchHooks, rememberLocationData, recallLocationMemory, forgetLocationMemory, showLocationMemoryStatus } from './lib/cli.js'; import { mcpManager } from './lib/mcp.js'; import { setMCPEnabled } from './lib/config.js'; import { resolveHook, isHookReference } from './lib/hooks.js'; import { memoryManager } from './lib/memory.js'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; // Get package.json for version const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8')); class Langterm { constructor() { this.config = null; this.args = process.argv.slice(2); this.mcpManager = mcpManager; this.verbose = false; // Debug: Log raw arguments in development if (process.env.DEBUG) { console.log('Raw args:', this.args); } } async executeCommand(command) { return new Promise((resolve, reject) => { const child = spawn(command, [], { shell: true, stdio: 'inherit' }); child.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Command exited with code ${code}`)); } }); child.on('error', (error) => { reject(error); }); }); } async setup() { console.log(chalk.blue('šŸš€ Welcome to Langterm Setup!\n')); if (!await checkOllama()) { console.log(chalk.red('āŒ Ollama is not running!')); console.log(chalk.yellow('\nPlease start Ollama first:')); console.log(chalk.gray(' 1. Install from https://ollama.com')); console.log(chalk.gray(' 2. Run: ollama serve')); console.log(chalk.gray(' 3. Pull a model: ollama pull codestral:22b')); process.exit(1); } const models = await getModels(); if (models.length === 0) { console.log(chalk.red('āŒ No models found!')); console.log(chalk.yellow('\nPlease pull a model first:')); console.log(chalk.gray(' ollama pull codestral:22b')); console.log(chalk.gray(' or')); console.log(chalk.gray(' ollama pull deepseek-coder:6.7b')); process.exit(1); } const selectedModel = await selectModel(models); await saveConfig({ model: selectedModel }); this.config = { model: selectedModel }; console.log(chalk.green(`\nāœ… Setup complete! Using model: ${selectedModel}`)); console.log(chalk.gray('\nYou can now use langterm:')); console.log(chalk.gray(' langterm "list all files larger than 100MB"')); } async run() { // Parse command line arguments const parsedArgs = parseArgs(this.args); // Handle early exit flags if (parsedArgs.showHelp) { showHelp(); return; } if (parsedArgs.showVersion) { console.log(`langterm version ${packageJson.version}`); return; } if (parsedArgs.runSetup) { await this.setup(); return; } if (parsedArgs.runMCPSetup) { await setupMCPServers(); return; } if (parsedArgs.showMCPStatus) { await showMCPStatus(this.mcpManager); return; } if (parsedArgs.enableMCP) { await setMCPEnabled(true); console.log(chalk.green('āœ… MCP enabled')); return; } if (parsedArgs.disableMCP) { await setMCPEnabled(false); console.log(chalk.green('āœ… MCP disabled')); return; } if (parsedArgs.createHook) { await createHook(parsedArgs.hookName); return; } if (parsedArgs.showHooksList) { await showHooksList(); return; } if (parsedArgs.editHook) { await editHook(parsedArgs.hookName); return; } if (parsedArgs.deleteHook) { await deleteHook(parsedArgs.hookName); return; } if (parsedArgs.searchHooks) { await searchHooks(parsedArgs.hookName); return; } if (parsedArgs.rememberData) { await rememberLocationData(parsedArgs.memoryData); return; } if (parsedArgs.recallMemory) { await recallLocationMemory(); return; } if (parsedArgs.forgetMemory) { await forgetLocationMemory(); return; } if (parsedArgs.showMemoryStatus) { await showLocationMemoryStatus(); return; } // Set verbose mode if (parsedArgs.verbose) { this.verbose = true; } // Load config after checking for help/version/setup this.config = await loadConfig(); // Use model override if provided if (parsedArgs.modelOverride) { this.config = { ...this.config, model: parsedArgs.modelOverride }; } // First-time setup if no config exists if (!this.config && !parsedArgs.modelOverride) { console.log(chalk.yellow('No configuration found. Running setup...\n')); await this.setup(); console.log(chalk.gray('\n---\n')); } // Initialize MCP if enabled if (this.config?.mcp?.enabled) { try { await this.mcpManager.initialize(this.config.mcp); } catch (error) { console.log(chalk.yellow(`āš ļø MCP initialization failed: ${error.message}`)); console.log(chalk.gray('Continuing without MCP support...')); } } // Check Ollama is running if (!await checkOllama()) { console.log(chalk.red('āŒ Ollama is not running!')); console.log(chalk.yellow('Please start Ollama: ollama serve')); process.exit(1); } // Get user input let userInput = parsedArgs.userInput; if (!userInput) { userInput = await prompt('Enter your command in English: '); } if (!userInput || !userInput.trim()) { console.log(chalk.red('No input provided.')); process.exit(1); } // Resolve hook references (e.g., /backup -> content of backup.md) if (isHookReference(userInput)) { try { if (this.verbose) { console.log(chalk.gray(`šŸŖ Resolving hook: ${userInput}`)); } userInput = await resolveHook(userInput); if (this.verbose) { console.log(chalk.gray(`šŸŖ Hook resolved to: ${userInput}`)); } } catch (error) { console.log(chalk.red(`Hook error: ${error.message}`)); process.exit(1); } } // Intelligent MCP routing try { let executionResult = null; // Analyze intent if MCP is enabled if (this.config?.mcp?.enabled && this.mcpManager.isInitialized) { // Only show analyzing message if we have MCP tools const availableTools = await this.mcpManager.getAvailableTools(); if (availableTools.length > 0) { console.log(chalk.gray('Analyzing request with MCP tools...')); } const intentAnalysis = await this.mcpManager.analyzeUserIntent(userInput); executionResult = await this.mcpManager.executeIntent(userInput, intentAnalysis); // If MCP tool was executed successfully, we're done if (executionResult.type === 'mcp_tool_executed') { console.log(chalk.green('\nāœ… Request completed using MCP tools.')); return; } } // For terminal commands or fallbacks, generate command let mcpContext = ''; if (executionResult && executionResult.mcpContext) { mcpContext = executionResult.mcpContext; } else if (this.config?.mcp?.enabled && this.mcpManager.isInitialized) { mcpContext = await this.mcpManager.getEnhancedContext(userInput); } // Get memory context for current project let memoryContext = ''; try { memoryContext = await memoryManager.getEnhancedContext(userInput); } catch (error) { // Memory errors shouldn't break command generation if (this.verbose) { console.log(chalk.gray(`Memory context unavailable: ${error.message}`)); } } // Combine contexts let enhancedContext = ''; if (mcpContext && memoryContext) { enhancedContext = `${memoryContext}\n\nMCP Context:\n${mcpContext}`; } else if (memoryContext) { enhancedContext = memoryContext; } else if (mcpContext) { enhancedContext = mcpContext; } if (enhancedContext) { const contextSources = []; if (memoryContext) contextSources.push('🧠 project memory'); if (mcpContext) contextSources.push('šŸ“” MCP context'); console.log(chalk.gray(`Using enhanced context: ${contextSources.join(', ')}...`)); if (this.verbose) { console.log(chalk.gray('Enhanced context preview:')); console.log(chalk.gray(enhancedContext.slice(0, 200) + (enhancedContext.length > 200 ? '...' : ''))); } } const command = await generateCommand(userInput, this.config.model, enhancedContext); if (!command || command === 'null') { console.log(chalk.red('Failed to generate a command.')); process.exit(1); } console.log(chalk.green(`\nSuggested command: ${command}`)); // Check for dangerous or warning patterns const safety = checkDangerousCommand(command); let shouldExecute = false; if (safety.isDangerous) { // Dangerous command requires explicit confirmation shouldExecute = await confirmDangerousCommand(command, safety.description); } else if (safety.isWarning) { // Warning command requires confirmation shouldExecute = await confirmWarningCommand(command, safety.description); } else { // Safe command - normal confirmation console.log(chalk.yellow('\nPress Enter to execute, Ctrl+C to cancel.')); const confirm = await prompt(''); shouldExecute = (confirm === ''); } if (shouldExecute) { console.log(chalk.gray(`Running: ${command}\n`)); try { await this.executeCommand(command); // Record successful command for pattern learning try { await memoryManager.recordSuccessfulCommand(userInput, command); } catch (error) { // Don't break flow if memory recording fails if (this.verbose) { console.log(chalk.gray(`Failed to record command for learning: ${error.message}`)); } } } catch (error) { // Command execution failed, don't record it throw error; } } else { console.log(chalk.red('Cancelled.')); } } catch (error) { console.log(chalk.red(`Error: ${error.message}`)); process.exit(1); } finally { // Clean up MCP connections if (this.mcpManager.isInitialized) { try { await this.mcpManager.disconnect(); } catch (error) { // Ignore cleanup errors } } } } } // Main execution const langterm = new Langterm(); langterm.run().catch(error => { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); });