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
172 lines (150 loc) • 6.1 kB
JavaScript
/**
* Intent Classification System - MCP-First Approach
*
* This system prioritizes MCP tools when available and falls back to
* terminal commands only when no suitable MCP tools are found.
*/
/**
* Intent types that help determine how to handle user requests
*/
export const INTENT_TYPES = {
MCP_TOOL_DIRECT: 'mcp_tool_direct', // Direct MCP tool execution
TERMINAL_COMMAND: 'terminal_command', // Traditional terminal command
HYBRID: 'hybrid', // Both MCP tool + terminal command
AMBIGUOUS: 'ambiguous' // Needs clarification
};
/**
* Get human-readable description for an intent type
*/
export function getIntentDescription(intent) {
switch (intent) {
case INTENT_TYPES.MCP_TOOL_DIRECT:
return 'MCP Tool Direct Execution';
case INTENT_TYPES.TERMINAL_COMMAND:
return 'Terminal Command Generation';
case INTENT_TYPES.HYBRID:
return 'Hybrid Approach';
case INTENT_TYPES.AMBIGUOUS:
return 'Ambiguous - needs clarification';
default:
return 'Unknown intent';
}
}
/**
* Analyzes user input to determine the most appropriate intent
* MCP-first approach: Always try to match with MCP tools first
*
* @param {string} userInput - Natural language input from user
* @param {Array} availableTools - Available MCP tools
* @returns {object} Intent analysis result
*/
export function analyzeIntent(userInput, availableTools = []) {
const input = userInput.toLowerCase().trim();
// First, check if we have any MCP tools available
if (availableTools.length > 0) {
// Find relevant MCP tools based on the input
const relevantTools = findRelevantTools(input, availableTools);
if (relevantTools.length > 0) {
// We found matching MCP tools - prefer using them
return {
intent: INTENT_TYPES.MCP_TOOL_DIRECT,
confidence: calculateToolRelevance(relevantTools),
scores: {
[INTENT_TYPES.MCP_TOOL_DIRECT]: calculateToolRelevance(relevantTools),
[INTENT_TYPES.TERMINAL_COMMAND]: 0.2, // Low score since we have MCP tools
[INTENT_TYPES.HYBRID]: 0.1
},
reasoning: `Found ${relevantTools.length} relevant MCP tool(s) that can handle this request`,
suggestedTools: relevantTools,
isAmbiguous: false
};
}
}
// No MCP tools available or no matching tools - fall back to terminal command
return {
intent: INTENT_TYPES.TERMINAL_COMMAND,
confidence: 0.8, // High confidence for terminal fallback
scores: {
[INTENT_TYPES.MCP_TOOL_DIRECT]: 0,
[INTENT_TYPES.TERMINAL_COMMAND]: 0.8,
[INTENT_TYPES.HYBRID]: 0.1
},
reasoning: availableTools.length === 0
? 'No MCP tools available, using terminal command'
: 'No matching MCP tools found, falling back to terminal command',
suggestedTools: [],
isAmbiguous: false
};
}
/**
* Find MCP tools relevant to the user's input
* Uses intelligent matching based on tool names and descriptions
*/
export function findRelevantTools(input, availableTools) {
if (!availableTools || availableTools.length === 0) {
return [];
}
const inputWords = input.toLowerCase().split(/\s+/);
return availableTools
.map(tool => {
let score = 0;
const toolName = (tool.name || '').toLowerCase();
const toolDesc = (tool.description || '').toLowerCase();
// Check if tool name matches any part of the input
if (inputWords.some(word => toolName.includes(word) || word.includes(toolName))) {
score += 0.5;
}
// Check if input contains tool name
if (input.includes(toolName)) {
score += 0.5;
}
// Check description relevance
const descWords = toolDesc.split(/\s+/);
const matchingWords = inputWords.filter(word =>
descWords.some(descWord =>
descWord.includes(word) || word.includes(descWord)
)
);
score += (matchingWords.length / inputWords.length) * 0.3;
// Special cases for common operations
// These are not hardcoded patterns but intelligent matches
if (tool.name.includes('list') && input.includes('list')) score += 0.2;
if (tool.name.includes('read') && (input.includes('read') || input.includes('show') || input.includes('display'))) score += 0.2;
if (tool.name.includes('write') && (input.includes('write') || input.includes('save') || input.includes('create'))) score += 0.2;
if (tool.name.includes('search') && (input.includes('search') || input.includes('find') || input.includes('grep'))) score += 0.2;
return { ...tool, relevanceScore: score };
})
.filter(tool => tool.relevanceScore > 0.1) // Only include tools with some relevance
.sort((a, b) => b.relevanceScore - a.relevanceScore)
.slice(0, 5); // Return top 5 most relevant tools
}
/**
* Calculate overall relevance score for a set of tools
*/
function calculateToolRelevance(tools) {
if (!tools || tools.length === 0) return 0;
// Use the highest scoring tool's relevance
const maxScore = Math.max(...tools.map(t => t.relevanceScore || 0));
// Boost confidence if multiple tools are relevant
const boost = Math.min(tools.length * 0.1, 0.3);
return Math.min(maxScore + boost, 1.0);
}
/**
* Generate reasoning explanation for the intent decision
* Simplified to focus on MCP-first approach
*/
export function generateReasoning(input, scores, availableTools) {
const hasTools = availableTools && availableTools.length > 0;
if (!hasTools) {
return 'No MCP tools available, defaulting to terminal command generation';
}
const mcpScore = scores[INTENT_TYPES.MCP_TOOL_DIRECT] || 0;
const terminalScore = scores[INTENT_TYPES.TERMINAL_COMMAND] || 0;
if (mcpScore > terminalScore) {
return 'MCP tools can handle this request directly';
} else if (terminalScore > mcpScore) {
return 'No suitable MCP tools found for this request, using terminal command';
} else {
return 'Request requires clarification';
}
}