UNPKG

@michaelnkomo/cli

Version:

BroCode CLI - AI coding assistant with @ file tagging and multi-language support

248 lines (247 loc) 8.72 kB
/** * File Tagging System for Context Inclusion * Handles @ symbol detection and file/directory autocomplete */ import fs from 'node:fs/promises'; import path from 'node:path'; import { glob } from 'glob'; export class FileTaggingSystem { options; fileCache = new Map(); contentCache = new Map(); constructor(options = {}) { this.options = { workingDirectory: process.cwd(), maxFileSize: 1024 * 1024, // 1MB excludePatterns: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '*.log', '.env*', '*.min.js', '*.map' ], includeHidden: false, ...options }; } /** * Discover all files and directories in the working directory */ async discoverFiles() { const cacheKey = this.options.workingDirectory; if (this.fileCache.has(cacheKey)) { return this.fileCache.get(cacheKey); } try { const files = []; // Use glob to find all files, respecting gitignore and exclude patterns const globPattern = this.options.includeHidden ? '**/*' : '**/[!.]*'; const foundPaths = await glob(globPattern, { cwd: this.options.workingDirectory, ignore: this.options.excludePatterns, dot: this.options.includeHidden, nodir: false, // Include directories }); for (const relativePath of foundPaths) { const fullPath = path.join(this.options.workingDirectory, relativePath); try { const stats = await fs.stat(fullPath); // Skip files that are too large if (stats.isFile() && stats.size > this.options.maxFileSize) { continue; } const fileItem = { name: path.basename(relativePath), path: relativePath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.isFile() ? stats.size : undefined, extension: stats.isFile() ? path.extname(relativePath).slice(1) : undefined }; files.push(fileItem); } catch (error) { // Skip files we can't access continue; } } // Sort files: directories first, then files, alphabetically files.sort((a, b) => { if (a.type !== b.type) { return a.type === 'directory' ? -1 : 1; } return a.name.localeCompare(b.name); }); this.fileCache.set(cacheKey, files); return files; } catch (error) { console.error('Error discovering files:', error); return []; } } /** * Filter files based on user input */ async filterFiles(query) { const allFiles = await this.discoverFiles(); if (!query || query.length === 0) { return allFiles.slice(0, 20); // Return first 20 items } const lowerQuery = query.toLowerCase(); return allFiles .filter(file => file.name.toLowerCase().includes(lowerQuery) || file.path.toLowerCase().includes(lowerQuery)) .slice(0, 20); // Limit results for performance } /** * Read file content for context inclusion */ async readFileContent(filePath) { const fullPath = path.resolve(this.options.workingDirectory, filePath); // Check cache first if (this.contentCache.has(fullPath)) { return this.contentCache.get(fullPath); } try { const stats = await fs.stat(fullPath); if (stats.isDirectory()) { // For directories, return a listing const dirContents = await this.getDirectoryListing(fullPath); this.contentCache.set(fullPath, dirContents); return dirContents; } if (!stats.isFile()) { return null; } // Check file size if (stats.size > this.options.maxFileSize) { return `File too large (${this.formatFileSize(stats.size)}). Showing first 1000 characters:\n\n${(await fs.readFile(fullPath, 'utf-8')).slice(0, 1000)}...`; } // Check if it's a binary file if (this.isBinaryFile(filePath)) { return `Binary file: ${filePath} (${this.formatFileSize(stats.size)})`; } const content = await fs.readFile(fullPath, 'utf-8'); this.contentCache.set(fullPath, content); return content; } catch (error) { return `Error reading file: ${error.message}`; } } /** * Get directory listing as a formatted string */ async getDirectoryListing(dirPath) { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); const listing = items .map(item => { const type = item.isDirectory() ? '📁' : '📄'; return `${type} ${item.name}`; }) .join('\n'); return `Directory listing for: ${path.relative(this.options.workingDirectory, dirPath)}\n\n${listing}`; } catch (error) { return `Error reading directory: ${error.message}`; } } /** * Check if a file is binary based on extension */ isBinaryFile(filePath) { const binaryExtensions = [ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg', '.mp3', '.mp4', '.avi', '.mov', '.wmv', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.zip', '.tar', '.gz', '.rar', '.7z', '.exe', '.dll', '.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot' ]; const ext = path.extname(filePath).toLowerCase(); return binaryExtensions.includes(ext); } /** * Format file size in human-readable format */ formatFileSize(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } /** * Parse @ tags from user input */ parseFileTags(input) { const fileTagRegex = /@([\w\-./\\]+(?:\.[a-zA-Z0-9]+)?)/g; const fileTags = []; let match; while ((match = fileTagRegex.exec(input)) !== null) { fileTags.push(match[1]); } // Remove file tags from input const cleanInput = input.replace(fileTagRegex, '').trim(); return { cleanInput, fileTags }; } /** * Build context from tagged files */ async buildFileContext(fileTags) { if (fileTags.length === 0) { return ''; } const contextParts = []; contextParts.push('📁 **FILE CONTEXT:**\n'); for (const fileTag of fileTags) { const content = await this.readFileContent(fileTag); if (content !== null) { contextParts.push(`\n## 📄 ${fileTag}\n\`\`\`\n${content}\n\`\`\`\n`); } else { contextParts.push(`\n## ❌ ${fileTag}\nFile not found or could not be read.\n`); } } return contextParts.join(''); } /** * Clear caches */ clearCache() { this.fileCache.clear(); this.contentCache.clear(); } /** * Update working directory and clear cache */ setWorkingDirectory(directory) { this.options.workingDirectory = path.resolve(directory); this.clearCache(); } } /** * Global file tagging system instance */ export const fileTagging = new FileTaggingSystem(); /** * Helper function to detect @ symbols in input */ export function detectFileTagging(input) { return input.includes('@') && /@[\w\-./\\]*/.test(input); } /** * Helper function to get current @ query being typed */ export function getCurrentTagQuery(input, cursorPosition) { const beforeCursor = input.slice(0, cursorPosition); const match = beforeCursor.match(/@([\w\-./\\]*)$/); return match ? match[1] : null; }