const fs = require('fs').promises; const path = require('path'); const readline = require('readline'); const { exec } = require('child_process'); class LMStudioFileAgent { constructor(options = {}) { this.lmStudioUrl = options.lmStudioUrl || 'http://localhost:1234/v1'; this.workspaceDir = path.resolve(options.workspaceDir || './workspace'); this.modelName = options.modelName || 'local-model'; this.conversationHistory = []; // Create workspace directory this._ensureDirectory(this.workspaceDir); // Test connection this._testConnection(); } async _ensureDirectory(dir) { try { await fs.mkdir(dir, { recursive: true }); } catch (error) { console.error(`Error creating directory ${dir}:`, error); } } async _testConnection() { try { const response = await fetch(`${this.lmStudioUrl}/models`); if (response.ok) { const models = await response.json(); console.log(`✓ Connected to LM Studio at ${this.lmStudioUrl}`); console.log(`✓ Available models:`, models); } else { console.log(`⚠ Warning: Could not connect to LM Studio (Status: ${response.status})`); } } catch (error) { console.log(`⚠ Warning: LM Studio connection failed: ${error.message}`); console.log(' Make sure LM Studio is running with the local server enabled'); } } async queryLLM(prompt, systemPrompt = null, maxTokens = 2000) { const messages = []; if (systemPrompt) { messages.push({ role: 'system', content: systemPrompt }); } // Add recent conversation history (last 10 messages) messages.push(...this.conversationHistory.slice(-10)); messages.push({ role: 'user', content: prompt }); try { const response = await fetch(`${this.lmStudioUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: this.modelName, messages: messages, temperature: 0.7, max_tokens: maxTokens, stream: false }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); const content = result.choices[0].message.content; // Update conversation history this.conversationHistory.push({ role: 'user', content: prompt }); this.conversationHistory.push({ role: 'assistant', content: content }); return content; } catch (error) { console.error('Error querying LLM:', error.message); return `Error: ${error.message}`; } } // ============ FILE OPERATIONS ============ async readFile(filepath) { try { const fullPath = this._resolvePath(filepath); const content = await fs.readFile(fullPath, 'utf-8'); return content; } catch (error) { if (error.code === 'ENOENT') { return `Error: File '${filepath}' not found`; } return `Error reading file: ${error.message}`; } } async writeFile(filepath, content, mode = 'w') { try { const fullPath = this._resolvePath(filepath); await this._ensureDirectory(path.dirname(fullPath)); if (mode === 'a') { await fs.appendFile(fullPath, content, 'utf-8'); } else { await fs.writeFile(fullPath, content, 'utf-8'); } return `✓ Successfully wrote to '${filepath}'`; } catch (error) { return `Error writing file: ${error.message}`; } } async editFile(filepath, oldContent, newContent) { try { const fullPath = this._resolvePath(filepath); const content = await fs.readFile(fullPath, 'utf-8'); if (!content.includes(oldContent)) { return `Error: Could not find the specified content in '${filepath}'`; } const updatedContent = content.replace(oldContent, newContent); await fs.writeFile(fullPath, updatedContent, 'utf-8'); return `✓ Successfully edited '${filepath}'`; } catch (error) { return `Error editing file: ${error.message}`; } } async applyCodeChanges(filepath, changes) { try { const fullPath = this._resolvePath(filepath); let content = await fs.readFile(fullPath, 'utf-8'); let applied = 0; for (const change of changes) { const oldContent = change.old || ''; const newContent = change.new || ''; if (content.includes(oldContent)) { content = content.replace(oldContent, newContent); applied++; } } await fs.writeFile(fullPath, content, 'utf-8'); return `✓ Applied ${applied}/${changes.length} changes to '${filepath}'`; } catch (error) { return `Error applying changes: ${error.message}`; } } async createFile(filepath, content = '', fileType = null) { try { const fullPath = this._resolvePath(filepath); try { await fs.access(fullPath); return `Error: File '${filepath}' already exists`; } catch { // File doesn't exist, continue } await this._ensureDirectory(path.dirname(fullPath)); let fileContent = content; if (fileType) { fileContent = this._getTemplate(fileType) + content; } await fs.writeFile(fullPath, fileContent, 'utf-8'); return `✓ Created new file '${filepath}'`; } catch (error) { return `Error creating file: ${error.message}`; } } async deleteFile(filepath) { try { const fullPath = this._resolvePath(filepath); await fs.unlink(fullPath); return `✓ Deleted '${filepath}'`; } catch (error) { if (error.code === 'ENOENT') { return `Error: File '${filepath}' not found`; } return `Error deleting file: ${error.message}`; } } // ============ DIRECTORY OPERATIONS ============ async listDirectory(directory = '.') { try { const fullPath = this._resolvePath(directory); const items = await fs.readdir(fullPath, { withFileTypes: true }); const result = []; for (const item of items) { const itemPath = path.join(fullPath, item.name); const stats = await fs.stat(itemPath); if (item.isDirectory()) { result.push(`📁 ${item.name}/`); } else { result.push(`📄 ${item.name} (${this._formatSize(stats.size)})`); } } return result.sort(); } catch (error) { return [`Error listing directory: ${error.message}`]; } } async searchFiles(pattern, directory = '.', recursive = true) { try { const fullPath = this._resolvePath(directory); const results = []; const searchRecursive = async (dir) => { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const itemPath = path.join(dir, item.name); if (item.isDirectory() && recursive) { await searchRecursive(itemPath); } else if (item.isFile()) { // Simple glob pattern matching if (this._matchPattern(item.name, pattern)) { results.push(path.relative(this.workspaceDir, itemPath)); } } } }; await searchRecursive(fullPath); return results.length > 0 ? results : ['No files found']; } catch (error) { return [`Error searching files: ${error.message}`]; } } async searchContent(searchText, directory = '.', filePattern = '*') { try { const fullPath = this._resolvePath(directory); const matches = []; const textExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.txt', '.md', '.json', '.html', '.css', '.yaml', '.yml', '.xml', '.sql']; const searchRecursive = async (dir) => { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const itemPath = path.join(dir, item.name); if (item.isDirectory()) { await searchRecursive(itemPath); } else if (item.isFile() && textExtensions.includes(path.extname(item.name))) { try { const content = await fs.readFile(itemPath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { if (line.toLowerCase().includes(searchText.toLowerCase())) { matches.push({ file: path.relative(this.workspaceDir, itemPath), line: index + 1, content: line.trim() }); } }); } catch { // Skip files that can't be read } } } }; await searchRecursive(fullPath); return matches; } catch (error) { return [{ error: error.message }]; } } // ============ AGENT INTELLIGENCE ============ async processRequest(userRequest) { const systemPrompt = `You are a file management agent. You can help with: 1. Reading files - Use read_file(filepath) 2. Writing files - Use write_file(filepath, content) 3. Editing files - Use edit_file(filepath, old_content, new_content) 4. Creating files - Use create_file(filepath, content) 5. Deleting files - Use delete_file(filepath) 6. Listing directories - Use list_directory(directory) 7. Searching files - Use search_files(pattern, directory) 8. Searching content - Use search_content(text, directory) Parse user requests and identify which operations to perform. Return a JSON response with the operations to execute.`; const prompt = `User request: ${userRequest} Available operations: ${JSON.stringify({ read_file: "Read file contents", write_file: "Write to file", edit_file: "Edit file content", create_file: "Create new file", delete_file: "Delete file", list_directory: "List directory contents", search_files: "Search for files", search_content: "Search within files" }, null, 2)} Respond with a JSON array of operations to perform: [{"operation": "read_file", "args": {"filepath": "example.txt"}}]`; try { const llmResponse = await this.queryLLM(prompt, systemPrompt); // Try to parse JSON from response const jsonMatch = llmResponse.match(/\[.*\]/s); if (jsonMatch) { const operations = JSON.parse(jsonMatch[0]); const results = []; for (const op of operations) { const result = await this._executeOperation(op); results.push(result); } return results.join('\n'); } } catch (error) { // Fall back to manual parsing } // Manual operation parsing return this._manualOperation(userRequest); } async _executeOperation(operation) { const opType = operation.operation; const args = operation.args || {}; const operationMap = { read_file: () => this.readFile(args.filepath), write_file: () => this.writeFile(args.filepath, args.content, args.mode), edit_file: () => this.editFile(args.filepath, args.old_content, args.new_content), create_file: () => this.createFile(args.filepath, args.content, args.file_type), delete_file: () => this.deleteFile(args.filepath), list_directory: () => this.listDirectory(args.directory), search_files: () => this.searchFiles(args.pattern, args.directory), search_content: () => this.searchContent(args.text, args.directory) }; if (operationMap[opType]) { try { const result = await operationMap[opType](); if (Array.isArray(result)) { return result.join('\n'); } return String(result); } catch (error) { return `Error executing ${opType}: ${error.message}`; } } return `Unknown operation: ${opType}`; } async _manualOperation(request) { const requestLower = request.toLowerCase(); const results = []; // Read file if (requestLower.includes('read') && requestLower.includes('file')) { const match = request.match(/["']([^"']+)["']/); if (match) { results.push(`Reading file: ${match[1]}`); results.push(await this.readFile(match[1])); } } // List directory if (requestLower.includes('list') || requestLower.includes('show')) { if (requestLower.includes('directory') || requestLower.includes('folder') || requestLower.includes('files')) { results.push('Directory contents:'); const items = await this.listDirectory(); results.push(...items); } } // Search files if (requestLower.includes('search') || requestLower.includes('find')) { if (requestLower.includes('file')) { const match = request.match(/["']([^"']+)["']/); if (match) { results.push(`Search results for: ${match[1]}`); const files = await this.searchFiles(match[1]); results.push(...files); } } } // Create file if (requestLower.includes('create') || requestLower.includes('new file')) { const match = request.match(/(?:create|new file)\s+["']?([^"']+)["']?/); if (match) { const filename = match[1].trim(); results.push(`Creating file: ${filename}`); results.push(await this.createFile(filename)); } } // If no manual operations matched, use LLM if (results.length === 0) { results.push(await this.queryLLM(request)); } return results.join('\n'); } // ============ UTILITY FUNCTIONS ============ _resolvePath(filepath) { return path.isAbsolute(filepath) ? filepath : path.join(this.workspaceDir, filepath); } _formatSize(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } _matchPattern(filename, pattern) { // Convert glob pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(filename); } _getTemplate(fileType) { const templates = { javascript: "// JavaScript file\n\n", python: "#!/usr/bin/env python3\n\n", html: "\n\n
\n