organ-ai-zer
Version:
AI-powered file organizer CLI tool
424 lines ⢠20.9 kB
JavaScript
;
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