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