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