UNPKG

organ-ai-zer

Version:

AI-powered file organizer CLI tool

424 lines • 20.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractiveAIOrganizer = void 0; const inquirer_1 = __importDefault(require("inquirer")); const ora_1 = __importDefault(require("ora")); const config_service_1 = require("./config-service"); const ai_providers_1 = require("./ai-providers"); const file_scanner_1 = require("./file-scanner"); class InteractiveAIOrganizer { constructor(configService) { this.aiProvider = null; this.fileScanner = new file_scanner_1.FileScanner(); this.configService = configService || config_service_1.ConfigService.getInstance(); } async organizeWithConversation(files, baseDirectory, initialIntent) { const config = await this.configService.loadConfig(); await this.initializeAIProvider(config); // Initialize conversation context const context = { intent: initialIntent, clarifications: [], rejectedSuggestions: [], approvedPatterns: [], fileCategories: this.categorizeFiles(files) }; console.log('šŸ¤– Starting AI analysis and conversation...\n'); // Main conversation loop let attempt = 0; const maxAttempts = 5; while (attempt < maxAttempts) { attempt++; console.log(`šŸ”„ Analysis attempt ${attempt}/${maxAttempts}...\n`); try { // Process files in batches by type for better consistency and reliability console.log('šŸ¤– Processing files in batches by type for optimal consistency...'); const result = await this.generateBatchedSuggestions(files, baseDirectory, context); // Handle AI-generated clarification requests if (result.needsClarification) { console.log('\nšŸ¤” The AI needs some clarification to provide better suggestions:\n'); console.log(`šŸ“ ${result.needsClarification.reason}\n`); const shouldAnswer = await this.handleAIClarificationQuestions(result.needsClarification.questions, context); // If user skipped questions, proceed with current suggestions instead of asking again if (!shouldAnswer && result.suggestions.length > 0) { console.log('\nā­ļø Proceeding with available suggestions...\n'); // Continue to present suggestions below instead of restarting loop } else if (!shouldAnswer) { // No suggestions and questions were skipped - need more context await this.gatherMoreContext(context); continue; } else { // Questions were answered - generate new suggestions continue; } } if (result.suggestions.length === 0) { console.log('āŒ No suggestions generated. Let\'s try refining your requirements...\n'); await this.gatherMoreContext(context); continue; } // Present suggestions and gather feedback const feedback = await this.presentSuggestionsAndGetFeedback(result.suggestions, context); if (feedback.approved) { console.log('āœ… Organization plan approved!\n'); return result.suggestions; } // Process feedback and continue conversation await this.processFeedbackAndRefine(feedback, context); } catch (error) { console.error('āŒ Error during AI analysis:', error); const { shouldContinue } = await inquirer_1.default.prompt([ { type: 'list', name: 'shouldContinue', message: 'An error occurred. What would you like to do?', choices: [ { name: 'šŸ”„ Try again with different parameters', value: 'retry' }, { name: '🚪 Cancel and exit', value: 'cancel' } ] } ]); if (shouldContinue !== 'retry') { console.log('\n🚪 Operation cancelled by user.'); throw new Error('Operation cancelled by user'); } await this.gatherMoreContext(context); } } throw new Error('Maximum conversation attempts reached. Please try with a simpler organization request.'); } async generateBatchedSuggestions(files, baseDirectory, context) { // Group files by type for batch processing const fileGroups = this.groupFilesByType(files); const allSuggestions = []; let globalClarification; console.log(`šŸ“‹ Processing ${Object.keys(fileGroups).length} file type groups...`); // Process each group separately for (const [groupName, groupFiles] of Object.entries(fileGroups)) { if (groupFiles.length === 0) continue; console.log(`šŸ”„ Processing ${groupName}: ${groupFiles.length} files`); const spinner = (0, ora_1.default)(`Analyzing ${groupName} files...`).start(); try { const groupResult = await this.generateContextualSuggestions(groupFiles, baseDirectory, context, groupName); spinner.succeed(`āœ… ${groupName}: ${groupResult.suggestions.length} suggestions generated`); allSuggestions.push(...groupResult.suggestions); // Collect clarification questions (only from first group that has them) if (!globalClarification && groupResult.needsClarification) { globalClarification = groupResult.needsClarification; } } catch (error) { spinner.fail(`āŒ ${groupName}: ${error}`); // Use rule-based fallback for this entire group const fallbackSuggestions = groupFiles.map(file => this.createRuleBasedFallback(file)); allSuggestions.push(...fallbackSuggestions); console.warn(`āš ļø Using rule-based fallback for ${groupName} group`); } } return { suggestions: allSuggestions, needsClarification: globalClarification }; } async generateContextualSuggestions(files, baseDirectory, context, groupName) { // Analyze file types and detect projects for better context const fileTypeAnalysis = this.analyzeFileTypes(files); const groupContext = groupName ? `\n\nCURRENT BATCH: Processing ${groupName} files specifically. Focus on consistency within this ${groupName} category.` : ''; const response = await this.aiProvider.analyzeFiles({ files, baseDirectory, existingStructure: [], userPreferences: { intent: context.intent, clarifications: context.clarifications, rejectedPatterns: context.rejectedSuggestions.map(s => s.suggestedPath), approvedPatterns: context.approvedPatterns, fileTypeAnalysis: fileTypeAnalysis + groupContext // Add batch context } }); // Create a mapping of suggestions by filename const suggestionMap = new Map(); response.suggestions.forEach(suggestion => { const fileName = suggestion.suggestedPath.split('/').pop(); suggestionMap.set(fileName, suggestion); }); // Ensure ALL files have suggestions - create intelligent defaults for missing ones const allSuggestions = []; files.forEach(file => { const suggestion = suggestionMap.get(file.name); if (suggestion) { allSuggestions.push({ ...suggestion, file }); } else { // AI failed to provide suggestion - use rule-based fallback const ruleSuggestion = this.createRuleBasedFallback(file); allSuggestions.push(ruleSuggestion); console.warn(`āš ļø AI missed file: ${file.name}, using rule-based fallback`); } }); return { suggestions: allSuggestions, needsClarification: response.clarificationNeeded }; } async presentSuggestionsAndGetFeedback(suggestions, context) { // Show a sample of suggestions const sampleSize = Math.min(10, suggestions.length); const sampleSuggestions = suggestions.slice(0, sampleSize); console.log(`šŸ“‹ Proposed Organization (showing ${sampleSize} of ${suggestions.length} files):\n`); sampleSuggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion.file.name}`); console.log(` → ${suggestion.suggestedPath}`); console.log(` Reason: ${suggestion.reason}\n`); }); if (suggestions.length > sampleSize) { console.log(`... and ${suggestions.length - sampleSize} more files organized similarly.\n`); } // Get user feedback const { initialFeedback } = await inquirer_1.default.prompt([ { type: 'list', name: 'initialFeedback', message: 'How do these organization suggestions look?', choices: [ { name: 'āœ… Perfect! Apply this organization', value: 'approve' }, { name: 'šŸ”§ Good direction, but needs some adjustments', value: 'adjust' }, { name: 'āŒ Not what I want, let me explain differently', value: 'reject' }, { name: 'ā“ I need to see more examples first', value: 'more_examples' }, { name: '🚪 Cancel and exit', value: 'cancel' } ] } ]); switch (initialFeedback) { case 'approve': return { approved: true }; case 'cancel': console.log('\n🚪 Operation cancelled by user.'); throw new Error('Operation cancelled by user'); case 'more_examples': // Show more examples const remainingSuggestions = suggestions.slice(sampleSize, sampleSize + 10); if (remainingSuggestions.length > 0) { console.log('\nšŸ“‹ Additional Examples:\n'); remainingSuggestions.forEach((suggestion, index) => { console.log(`${sampleSize + index + 1}. ${suggestion.file.name}`); console.log(` → ${suggestion.suggestedPath}`); console.log(` Reason: ${suggestion.reason}\n`); }); } return this.presentSuggestionsAndGetFeedback(suggestions, context); case 'adjust': const { adjustmentFeedback } = await inquirer_1.default.prompt([ { type: 'input', name: 'adjustmentFeedback', message: 'What adjustments would you like? (be specific about folder structure, naming, etc.):', validate: (input) => input.trim().length > 5 || 'Please provide more specific feedback' } ]); return { approved: false, feedback: adjustmentFeedback }; case 'reject': const { rejectionFeedback } = await inquirer_1.default.prompt([ { type: 'input', name: 'rejectionFeedback', message: 'Please explain how you would prefer the files to be organized:', validate: (input) => input.trim().length > 10 || 'Please provide a more detailed explanation' } ]); return { approved: false, feedback: rejectionFeedback, selectedSuggestions: suggestions }; default: return { approved: false }; } } async processFeedbackAndRefine(feedback, context) { if (feedback.feedback) { context.clarifications.push(feedback.feedback); console.log('šŸ“ Added your feedback to the conversation context.\n'); } if (feedback.selectedSuggestions) { context.rejectedSuggestions.push(...feedback.selectedSuggestions); } // The AI will now determine if it needs clarification on the next iteration // No need for hardcoded questions } async handleAIClarificationQuestions(questions, context) { const { shouldAnswer } = await inquirer_1.default.prompt([ { type: 'list', name: 'shouldAnswer', message: 'Would you like to answer these questions to help improve the suggestions?', choices: [ { name: 'āœ… Yes, let me answer these questions', value: 'yes' }, { name: 'āŒ Skip questions and try to proceed anyway', value: 'skip' }, { name: '🚪 Cancel and exit', value: 'cancel' } ] } ]); if (shouldAnswer === 'cancel') { console.log('\n🚪 Operation cancelled by user.'); throw new Error('Operation cancelled by user'); } if (shouldAnswer === 'skip') { context.clarifications.push('User chose to skip clarification questions and proceed with available suggestions'); return false; // Indicate questions were skipped } // Ask each question that the AI generated for (let i = 0; i < questions.length; i++) { const { answer } = await inquirer_1.default.prompt([ { type: 'input', name: 'answer', message: `${i + 1}. ${questions[i]}`, validate: (input) => input.trim().length > 0 || 'Please provide an answer' } ]); context.clarifications.push(`Q: ${questions[i]} A: ${answer}`); } console.log('\nāœ… Thank you for the clarifications. Let me generate new suggestions...\n'); return true; // Indicate questions were answered } analyzeFileTypes(files) { const typeGroups = {}; const potentialProjects = []; files.forEach(file => { const category = this.fileScanner.getFileCategory(file); if (!typeGroups[category]) { typeGroups[category] = []; } typeGroups[category].push(file); // Detect potential project indicators if (file.name.includes('package.json') || file.name.includes('requirements.txt') || file.name.includes('Cargo.toml') || file.name.includes('pom.xml') || file.name.includes('README') || file.name.includes('.git')) { potentialProjects.push(`Detected project file: ${file.name}`); } }); const analysis = Object.entries(typeGroups).map(([category, categoryFiles]) => { const examples = categoryFiles.slice(0, 3).map(f => f.name).join(', '); return `${category}: ${categoryFiles.length} files (examples: ${examples}${categoryFiles.length > 3 ? '...' : ''})`; }).join('\n'); let projectAnalysis = ''; if (potentialProjects.length > 0) { projectAnalysis = `\n\nPROJECT DETECTION:\n${potentialProjects.join('\n')}\nRemember to move project files together as units!`; } return `File type distribution and project analysis for consistency planning:\n${analysis}${projectAnalysis}\n\nCONSISTENCY REQUIREMENTS:\n- Use IDENTICAL patterns for same file types (TV shows, movies, music, etc.)\n- Recognize media patterns: S01E01, (2019), artist names\n- Group related files/projects together\n- Never suggest "no change" unless truly optimal\n- Provide suggestions for ALL files`; } groupFilesByType(files) { const groups = { 'TV Shows': [], 'Movies': [], 'Music': [], 'Documents': [], 'Code Projects': [], 'Other': [] }; files.forEach(file => { const category = this.categorizeFileForBatching(file); groups[category].push(file); }); // Remove empty groups Object.keys(groups).forEach(key => { if (groups[key].length === 0) { delete groups[key]; } }); return groups; } categorizeFileForBatching(file) { const fileName = file.name.toLowerCase(); const extension = file.extension.toLowerCase(); // TV Show patterns (video files with episode/season indicators) if (extension.match(/\.(mkv|mp4|avi|mov)$/) && (fileName.match(/s\d+e\d+/) || fileName.match(/season\s+\d+/) || fileName.match(/episode/) || fileName.includes('ep'))) { return 'TV Shows'; } // Movie patterns (video files that seem like movies - have years or don't have episode patterns) if (extension.match(/\.(mkv|mp4|avi|mov)$/) && !fileName.match(/s\d+e\d+/) && !fileName.match(/season\s+\d+/)) { return 'Movies'; } // Music files if (extension.match(/\.(mp3|wav|flac|m4a|aac|ogg)$/)) { return 'Music'; } // Code projects (look for project indicators) if (fileName.includes('package.json') || fileName.includes('readme') || fileName.includes('.js') || fileName.includes('.css') || fileName.includes('.py') || fileName.includes('.html')) { return 'Code Projects'; } // Documents if (extension.match(/\.(pdf|docx|xlsx|txt|md)$/)) { return 'Documents'; } // Images and other files return 'Other'; } createRuleBasedFallback(file) { // Use the same rule-based organization logic as the regular organize command const category = this.fileScanner.getFileCategory(file); // Simple category-based organization (same as ai-organizer fallback) const suggestedPath = `${category}/${file.name}`; return { file, suggestedPath, reason: `Rule-based fallback: organized by file type (${category})`, confidence: 0.6 }; } async gatherMoreContext(context) { const { additionalContext } = await inquirer_1.default.prompt([ { type: 'input', name: 'additionalContext', message: 'Please provide more details about how you want your files organized:', validate: (input) => input.trim().length > 5 || 'Please provide more details' } ]); context.clarifications.push(additionalContext); } categorizeFiles(files) { const categories = {}; files.forEach(file => { const category = this.fileScanner.getFileCategory(file); if (!categories[category]) { categories[category] = []; } categories[category].push(file); }); return categories; } async initializeAIProvider(config) { const aiConfig = { apiKey: config.ai.apiKey, model: config.ai.model, maxTokens: Math.max(config.ai.maxTokens || 1000, 2000), // Higher token limit for conversations temperature: Math.max(config.ai.temperature || 0.3, 0.5), // Slightly higher temperature for creativity timeout: Math.max(config.ai.timeout || 90000, 120000) // Longer timeout for interactive mode (120s minimum) }; switch (config.ai.provider) { case 'openai': this.aiProvider = new ai_providers_1.OpenAIProvider(aiConfig); break; case 'anthropic': this.aiProvider = new ai_providers_1.AnthropicProvider(aiConfig); break; default: throw new Error(`Unsupported AI provider: ${config.ai.provider}`); } } } exports.InteractiveAIOrganizer = InteractiveAIOrganizer; //# sourceMappingURL=interactive-ai-organizer.js.map