UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

342 lines (341 loc) 12.3 kB
import * as fs from "node:fs/promises"; import * as path from "node:path"; import { log } from "../util/logging.js"; import { mcpClient } from "../mcp/client.js"; import { backgroundExecutor } from "../tools/background.js"; /** * Enhanced autocomplete system inspired by Claude Code's Tab completion * Supports file paths, @-mentions, commands, and context-aware suggestions */ export class AutocompleteEngine { context; fileCache = new Map(); lastCacheUpdate = 0; CACHE_TTL = 30000; // 30 seconds /** * Initialize with project context */ initialize(context) { this.context = context; log.info("Autocomplete engine initialized"); } /** * Get completions for the current input */ async getCompletions(line, cursorPos) { if (!this.context) return []; const beforeCursor = line.slice(0, cursorPos); const afterCursor = line.slice(cursorPos); // Handle different completion types if (this.isCommand(beforeCursor)) { return this.getCommandCompletions(beforeCursor); } if (this.isMention(beforeCursor)) { return this.getMentionCompletions(beforeCursor); } if (this.isFilePath(beforeCursor)) { return this.getFilePathCompletions(beforeCursor); } if (this.isBackgroundProcess(beforeCursor)) { return this.getBackgroundProcessCompletions(beforeCursor); } // Default: context-aware task suggestions return this.getTaskSuggestions(beforeCursor); } /** * Check if input is a command */ isCommand(input) { return input.startsWith('/') || input.startsWith('!'); } /** * Check if input contains @-mention */ isMention(input) { return /@[\w\-\.]*$/.test(input); } /** * Check if input is a file path */ isFilePath(input) { return /[\w\/\.\-~]+$/.test(input) && !input.startsWith('/') && !input.startsWith('@'); } /** * Check if input is referencing background processes */ isBackgroundProcess(input) { return /\/(bg|background)(-[\w-]+)?\s+[\w-]*$/.test(input); } /** * Get command completions */ getCommandCompletions(input) { const commands = [ // System commands "/provider", "/model", "/keys", "/health", "/whoami", "/budget", "/sessions", "/config", "/permissions", "/help", // Background commands "/bg", "/background", "/bg-list", "/bg-kill", "/bg-kill-all", "/bg-output", // Enhanced system commands "/hooks", "/security", "/diffs", "/intelligence", "/performance", "/plugins", "/suggestions", // MCP commands "/mcp", "/mcp-list", "/mcp-connect", "/mcp-disconnect", // Workspace commands "/workspace", "/bookmark", "/theme", // Git workflow commands "rollback", "merge", "test", "lint", "build", "log", "clear-log" ]; const partial = input.slice(1); // Remove the '/' prefix return commands.filter(cmd => cmd.slice(1).startsWith(partial)); } /** * Get @-mention completions */ async getMentionCompletions(input) { const mentionMatch = input.match(/@([\w\-\.]*)$/); if (!mentionMatch) return []; const partial = mentionMatch[1]; const completions = []; // Add file completions const fileCompletions = await this.getFileCompletions(partial); completions.push(...fileCompletions.map(f => `@${f}`)); // Add MCP resource completions try { const resources = await mcpClient.listResources(); const resourceCompletions = resources .filter(r => r.name.toLowerCase().includes(partial.toLowerCase())) .map(r => `@${r.name}`) .slice(0, 10); completions.push(...resourceCompletions); } catch (error) { // MCP not available, skip } return completions.slice(0, 20); // Limit to 20 suggestions } /** * Get file path completions */ async getFilePathCompletions(input) { const lastSpace = input.lastIndexOf(' '); const pathPart = input.slice(lastSpace + 1); if (!pathPart) return []; try { let basePath = this.context.repoPath; let searchTerm = pathPart; // Handle relative paths if (pathPart.includes('/')) { const pathSegments = pathPart.split('/'); searchTerm = pathSegments.pop() || ''; basePath = path.join(basePath, ...pathSegments.slice(0, -1)); } // Handle home directory if (pathPart.startsWith('~')) { basePath = process.env.HOME || '/'; searchTerm = pathPart.slice(2); } const items = await fs.readdir(basePath); const matches = items.filter(item => item.toLowerCase().startsWith(searchTerm.toLowerCase())); // Add directory indicators const completions = []; for (const match of matches.slice(0, 15)) { const fullPath = path.join(basePath, match); try { const stat = await fs.stat(fullPath); completions.push(stat.isDirectory() ? `${match}/` : match); } catch { completions.push(match); } } return completions; } catch (error) { return []; } } /** * Get background process completions */ getBackgroundProcessCompletions(input) { const processes = backgroundExecutor.listProcesses(); const commandMatch = input.match(/\/(bg|background)(-[\w-]+)?\s+([\w-]*)$/); if (!commandMatch) return []; const [, , subCommand, partial] = commandMatch; if (subCommand === '-kill' || subCommand === '-output') { // Complete with process IDs return processes .map(p => p.id) .filter(id => id.startsWith(partial)) .slice(0, 10); } // Default background commands const bgCommands = ['npm run dev', 'npm run build', 'pytest --watch', 'cargo watch', 'tsc --watch']; return bgCommands.filter(cmd => cmd.startsWith(partial)); } /** * Get file completions for @-mentions */ async getFileCompletions(partial) { if (!this.context) return []; const cacheKey = `files-${partial}`; const now = Date.now(); // Check cache if (this.fileCache.has(cacheKey) && (now - this.lastCacheUpdate) < this.CACHE_TTL) { return this.fileCache.get(cacheKey); } try { const files = await this.findFiles(this.context.repoPath, partial); this.fileCache.set(cacheKey, files); this.lastCacheUpdate = now; return files; } catch (error) { return []; } } /** * Find files matching partial string */ async findFiles(basePath, partial, maxResults = 20) { const results = []; const seen = new Set(); const searchDirectory = async (dir, maxDepth = 3) => { if (maxDepth <= 0 || results.length >= maxResults) return; try { const items = await fs.readdir(dir); for (const item of items) { if (results.length >= maxResults) break; // Skip hidden files and node_modules if (item.startsWith('.') || item === 'node_modules') continue; const fullPath = path.join(dir, item); const relativePath = path.relative(basePath, fullPath); if (seen.has(relativePath)) continue; seen.add(relativePath); // Check if matches partial if (item.toLowerCase().includes(partial.toLowerCase()) || relativePath.toLowerCase().includes(partial.toLowerCase())) { results.push(relativePath); } // Recurse into directories try { const stat = await fs.stat(fullPath); if (stat.isDirectory()) { await searchDirectory(fullPath, maxDepth - 1); } } catch { // Skip inaccessible directories } } } catch (error) { // Skip inaccessible directories } }; await searchDirectory(basePath); return results.slice(0, maxResults); } /** * Get context-aware task suggestions */ getTaskSuggestions(input) { if (!this.context || input.length < 2) return []; const suggestions = []; const inputLower = input.toLowerCase(); // Project-specific suggestions if (this.context.projectType === 'react' || this.context.projectType === 'next') { const reactSuggestions = [ "Add a new React component", "Create a custom hook", "Add TypeScript types", "Implement state management", "Add API integration", "Create tests for components", "Optimize performance", "Add error boundaries" ]; suggestions.push(...reactSuggestions.filter(s => s.toLowerCase().includes(inputLower))); } if (this.context.projectType === 'python') { const pythonSuggestions = [ "Add error handling", "Create unit tests", "Add type hints", "Optimize performance", "Add logging", "Create CLI interface", "Add database integration", "Implement async functions" ]; suggestions.push(...pythonSuggestions.filter(s => s.toLowerCase().includes(inputLower))); } // Generic suggestions const genericSuggestions = [ "Refactor code for better readability", "Add comprehensive error handling", "Implement proper logging", "Add input validation", "Create unit tests", "Add documentation", "Optimize performance", "Fix security vulnerabilities", "Add configuration management", "Implement caching" ]; suggestions.push(...genericSuggestions.filter(s => s.toLowerCase().includes(inputLower))); return suggestions.slice(0, 8); // Limit to 8 suggestions } /** * Clear file cache */ clearCache() { this.fileCache.clear(); this.lastCacheUpdate = 0; } /** * Get completion statistics */ getStats() { return { cacheSize: this.fileCache.size, lastCacheUpdate: this.lastCacheUpdate, cacheHits: 0 // Would need to implement hit tracking }; } } /** * Setup readline with enhanced autocomplete */ export function setupAutocomplete(rl, engine) { // Custom completer function const completer = async (line) => { try { const completions = await engine.getCompletions(line, line.length); return [completions, line]; } catch (error) { log.warn("Autocomplete error:", error); return [[], line]; } }; // Override the completer rl.completer = completer; // Handle Tab key for autocomplete rl.on('SIGINT', () => { // Clear cache on interrupt engine.clearCache(); }); } // Export singleton instance export const autocompleteEngine = new AutocompleteEngine();