commit 5fcd83fedb22662f68bd3ee7a86b40cc2f4d962a Author: melabidi Date: Tue Sep 1 05:44:00 2026 +0000 add first file diff --git a/Node.js b/Node.js new file mode 100644 index 0000000..c883059 --- /dev/null +++ b/Node.js @@ -0,0 +1,610 @@ +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 Document\n\n\n \n\n", + css: "/* CSS file */\n\n", + json: "{\n \n}", + markdown: "# Title\n\n", + text: "" + }; + + return templates[fileType.toLowerCase()] || ''; + } +} + +// ============ CLI INTERFACE ============ + +async function main() { + console.log('🤖 LM Studio File Agent (Node.js)'); + console.log('='.repeat(50)); + + // Initialize agent + const agent = new LMStudioFileAgent({ + workspaceDir: './workspace' + }); + + // Create readline interface + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + console.log('\nCommands:'); + console.log(' read - Read a file'); + console.log(' list - List directory contents'); + console.log(' create - Create a new file'); + console.log(' search - Search for files'); + console.log(' write - Write content to file'); + console.log(' edit - Edit a file'); + console.log(' help - Show this help'); + console.log(' exit - Quit the program\n'); + + const promptUser = () => { + rl.question('You: ', async (input) => { + const trimmedInput = input.trim(); + + if (['exit', 'quit', 'q'].includes(trimmedInput.toLowerCase())) { + console.log('Goodbye!'); + rl.close(); + return; + } + + if (trimmedInput.toLowerCase() === 'help') { + console.log('\nAvailable commands:'); + console.log(' read - Read a file'); + console.log(' list - List directory contents'); + console.log(' create - Create a new file'); + console.log(' search - Search for files'); + console.log(' write - Write content to file'); + console.log(' edit - Edit a file'); + console.log(' delete - Delete a file'); + console.log(' search content - Search within files'); + console.log(' exit - Quit the program\n'); + promptUser(); + return; + } + + if (!trimmedInput) { + promptUser(); + return; + } + + try { + // Parse command + const [command, ...args] = trimmedInput.split(' '); + const arg = args.join(' '); + + let result; + switch (command.toLowerCase()) { + case 'read': + result = await agent.readFile(arg); + break; + case 'list': + const items = await agent.listDirectory(); + result = items.join('\n'); + break; + case 'create': + result = await agent.createFile(arg); + break; + case 'write': + const [filepath, ...contentParts] = args; + const content = contentParts.join(' '); + if (filepath && content) { + result = await agent.writeFile(filepath, content); + } else { + result = 'Usage: write '; + } + break; + case 'edit': + const [editFile, oldContent, newContent] = arg.split('|').map(s => s.trim()); + if (editFile && oldContent && newContent) { + result = await agent.editFile(editFile, oldContent, newContent); + } else { + result = 'Usage: edit | | '; + } + break; + case 'delete': + result = await agent.deleteFile(arg); + break; + case 'search': + if (args[0] === 'content') { + const searchResults = await agent.searchContent(args[1] || ''); + result = searchResults.map(r => `${r.file}:${r.line} - ${r.content}`).join('\n'); + } else { + const files = await agent.searchFiles(arg); + result = files.join('\n'); + } + break; + default: + // Process as natural language request + result = await agent.processRequest(trimmedInput); + } + + console.log(`\nAgent:\n${result}`); + } catch (error) { + console.error(`Error: ${error.message}`); + } + + promptUser(); + }); + }; + + promptUser(); +} + +// Run if this file is executed directly +if (require.main === module) { + main().catch(console.error); +} + +// Export for use as a module +module.exports = LMStudioFileAgent; \ No newline at end of file