Files
llm_agent/Node.js
T
2026-09-01 05:44:00 +00:00

610 lines
22 KiB
JavaScript

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: "<!DOCTYPE html>\n<html>\n<head>\n <title>Document</title>\n</head>\n<body>\n \n</body>\n</html>",
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 <file> - Read a file');
console.log(' list - List directory contents');
console.log(' create <file> - Create a new file');
console.log(' search <pattern> - Search for files');
console.log(' write <file> - Write content to file');
console.log(' edit <file> - 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 <file> - Read a file');
console.log(' list - List directory contents');
console.log(' create <file> - Create a new file');
console.log(' search <pattern> - Search for files');
console.log(' write <file> - Write content to file');
console.log(' edit <file> - Edit a file');
console.log(' delete <file> - Delete a file');
console.log(' search content <text> - 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 <file> <content>';
}
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 <file> | <old content> | <new content>';
}
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;