UNPKG

cntx-ui

Version:

File context management tool with web UI and MCP server for AI development workflows - bundle project files for LLM consumption

368 lines (323 loc) 10.5 kB
/** * Agent Tools for Codebase Exploration * Built on top of existing cntx-ui infrastructure */ import { readFileSync, existsSync } from 'fs'; import { join, relative } from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export class AgentTools { constructor(cntxServer) { this.cntxServer = cntxServer; } /** * Read file contents with bundle context */ async readFile(filePath, options = {}) { try { const fullPath = join(this.cntxServer.CWD, filePath); if (!existsSync(fullPath)) { throw new Error(`File not found: ${filePath}`); } const content = readFileSync(fullPath, 'utf8'); const bundles = this.getFileBundles(filePath); return { path: filePath, content: options.truncate ? this.truncateContent(content, options.maxLength) : content, size: content.length, lines: content.split('\n').length, bundles, mimeType: this.getMimeType(filePath) }; } catch (error) { throw new Error(`Failed to read file ${filePath}: ${error.message}`); } } /** * List files with bundle awareness and filtering */ async listFiles(options = {}) { const { bundle, pattern, type, limit = 100 } = options; try { let files = []; if (bundle) { const bundleObj = this.cntxServer.bundles.get(bundle); if (!bundleObj) { throw new Error(`Bundle '${bundle}' not found`); } files = bundleObj.files.map(f => ({ path: f, bundle, size: this.getFileSize(f), type: this.getFileType(f) })); } else { // Get all files across bundles const allFiles = this.cntxServer.getAllFiles(); files = allFiles.map(f => ({ path: f, bundles: this.getFileBundles(f), size: this.getFileSize(f), type: this.getFileType(f) })); } // Apply filters if (pattern) { const regex = new RegExp(pattern, 'i'); files = files.filter(f => regex.test(f.path)); } if (type) { files = files.filter(f => f.type === type); } // Limit results return files.slice(0, limit); } catch (error) { throw new Error(`Failed to list files: ${error.message}`); } } /** * Search semantic chunks using existing vector search */ async searchSemanticChunks(query, options = {}) { try { const analysis = await this.cntxServer.getSemanticAnalysis(); if (!analysis || !analysis.chunks) { return { chunks: [], message: 'No semantic analysis available' }; } let chunks = analysis.chunks; const { bundle, type, complexity, maxResults = 10 } = options; // Apply semantic search if vector store is available if (this.cntxServer.vectorStoreInitialized) { try { const searchResults = await this.cntxServer.vectorStore.search(query, maxResults * 2); const chunkIds = searchResults.map(r => r.metadata?.chunkId).filter(Boolean); chunks = chunks.filter(c => chunkIds.includes(c.id)); } catch (error) { // Fall back to text-based search chunks = chunks.filter(c => c.purpose?.toLowerCase().includes(query.toLowerCase()) || c.name?.toLowerCase().includes(query.toLowerCase()) || c.code?.toLowerCase().includes(query.toLowerCase()) ); } } else { // Text-based semantic search chunks = chunks.filter(c => c.purpose?.toLowerCase().includes(query.toLowerCase()) || c.name?.toLowerCase().includes(query.toLowerCase()) || c.code?.toLowerCase().includes(query.toLowerCase()) ); } // Apply filters if (bundle) { chunks = chunks.filter(c => c.bundles && c.bundles.includes(bundle)); } if (type) { chunks = chunks.filter(c => c.subtype === type); } if (complexity) { chunks = chunks.filter(c => c.complexity?.level === complexity); } // Clean and limit results const cleanChunks = chunks.slice(0, maxResults).map(chunk => ({ ...chunk, code: this.truncateContent(chunk.code, 500), bundles: chunk.bundles || [], relevanceScore: chunk.score || 0 })); return { query, chunks: cleanChunks, totalResults: chunks.length, hasMore: chunks.length > maxResults }; } catch (error) { throw new Error(`Semantic search failed: ${error.message}`); } } /** * Get bundle information */ async getBundle(bundleName) { try { const bundle = this.cntxServer.bundles.get(bundleName); if (!bundle) { throw new Error(`Bundle '${bundleName}' not found`); } return { name: bundleName, patterns: bundle.patterns, files: bundle.files, fileCount: bundle.files.length, size: bundle.size, lastGenerated: bundle.lastGenerated, changed: bundle.changed, content: bundle.content ? this.truncateContent(bundle.content, 2000) : null }; } catch (error) { throw new Error(`Failed to get bundle: ${error.message}`); } } /** * Parse AST using existing tree-sitter infrastructure */ async parseAST(filePath, options = {}) { try { const fullPath = join(this.cntxServer.CWD, filePath); if (!existsSync(fullPath)) { throw new Error(`File not found: ${filePath}`); } // Use existing semantic analysis if available const analysis = await this.cntxServer.getSemanticAnalysis(); const fileChunks = analysis?.chunks?.filter(c => c.filePath === filePath) || []; if (fileChunks.length > 0) { return { file: filePath, chunks: fileChunks.map(chunk => ({ name: chunk.name, type: chunk.subtype, purpose: chunk.purpose, startLine: chunk.startLine, endLine: chunk.endLine, complexity: chunk.complexity, isExported: chunk.isExported, isAsync: chunk.isAsync, imports: chunk.includes?.imports || [], dependencies: chunk.dependencies || [] })) }; } // Fallback: basic file info const content = readFileSync(fullPath, 'utf8'); return { file: filePath, lines: content.split('\n').length, size: content.length, type: this.getFileType(filePath), message: 'AST parsing requires semantic analysis to be run first' }; } catch (error) { throw new Error(`AST parsing failed: ${error.message}`); } } /** * Execute safe CLI commands (restricted set) */ async runCommand(command, options = {}) { const { timeout = 10000, cwd } = options; // Whitelist of safe commands const safeCommands = [ 'ls', 'find', 'grep', 'wc', 'head', 'tail', 'git status', 'git log', 'git diff', 'git branch', 'npm list', 'npm outdated', 'npm audit', 'node --version', 'npm --version' ]; const isCommandSafe = safeCommands.some(safe => command.startsWith(safe)); if (!isCommandSafe) { throw new Error(`Command not allowed: ${command}. Only safe read-only commands are permitted.`); } try { const { stdout, stderr } = await execAsync(command, { cwd: cwd || this.cntxServer.CWD, timeout, maxBuffer: 1024 * 1024 // 1MB limit }); return { command, stdout: stdout.trim(), stderr: stderr.trim(), success: true }; } catch (error) { return { command, stdout: error.stdout || '', stderr: error.stderr || error.message, success: false, error: error.message }; } } /** * Get full semantic analysis */ async getSemanticAnalysis(options = {}) { try { const analysis = await this.cntxServer.getSemanticAnalysis(); if (!analysis) { return { message: 'No semantic analysis available. Run semantic analysis first.' }; } const { includeCode = false, maxChunks = 50 } = options; return { timestamp: analysis.timestamp, summary: analysis.summary, chunks: analysis.chunks.slice(0, maxChunks).map(chunk => ({ ...chunk, code: includeCode ? chunk.code : this.truncateContent(chunk.code, 200), bundles: chunk.bundles || [] })), totalChunks: analysis.chunks?.length || 0, truncated: analysis.chunks?.length > maxChunks }; } catch (error) { throw new Error(`Failed to get semantic analysis: ${error.message}`); } } // Helper methods getFileBundles(filePath) { const bundles = []; for (const [bundleName, bundle] of this.cntxServer.bundles) { if (bundle.files.includes(filePath)) { bundles.push(bundleName); } } return bundles; } getFileSize(filePath) { try { const fullPath = join(this.cntxServer.CWD, filePath); const stats = require('fs').statSync(fullPath); return stats.size; } catch { return 0; } } getFileType(filePath) { const ext = require('path').extname(filePath).toLowerCase(); const typeMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.json': 'json', '.md': 'markdown', '.css': 'css', '.html': 'html', '.py': 'python', '.java': 'java', '.go': 'go', '.rs': 'rust' }; return typeMap[ext] || 'text'; } getMimeType(filePath) { const ext = require('path').extname(filePath).toLowerCase(); const mimeTypes = { '.js': 'application/javascript', '.jsx': 'application/javascript', '.ts': 'application/typescript', '.tsx': 'application/typescript', '.json': 'application/json', '.md': 'text/markdown', '.css': 'text/css', '.html': 'text/html' }; return mimeTypes[ext] || 'text/plain'; } truncateContent(content, maxLength = 1000) { if (!content || content.length <= maxLength) return content; return content.substring(0, maxLength) + '...'; } } export default AgentTools;