add first file
This commit is contained in:
@@ -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: "<!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;
|
||||
Reference in New Issue
Block a user