UNPKG

organ-ai-zer

Version:

AI-powered file organizer CLI tool

246 lines 11 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AIOrganizer = void 0; 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"); const suggestion_cache_1 = require("./suggestion-cache"); const path = __importStar(require("path")); class AIOrganizer { constructor(configService) { this.aiProvider = null; this.fileScanner = new file_scanner_1.FileScanner(); this.cache = suggestion_cache_1.SuggestionCache.getInstance(); this.configService = configService || config_service_1.ConfigService.getInstance(); } async generateSuggestions(files, useCache = true) { const config = await this.configService.loadConfig(); // Validate API key if (!config.ai.apiKey) { throw new Error('No API key configured. Please run "organ-ai-zer init" and add your API key to the config file.'); } // Filter files based on config const filteredFiles = this.filterFiles(files, config); if (filteredFiles.length === 0) { console.log('📁 No files to organize after applying filters'); return []; } const baseDirectory = path.dirname(filteredFiles[0].path); const configHash = this.configService.getConfigHash(); // Check cache first if enabled if (useCache) { const cachedSuggestions = await this.cache.getCachedSuggestions(baseDirectory, filteredFiles, configHash); if (cachedSuggestions) { return cachedSuggestions; } } // Initialize AI provider with appropriate token count await this.initializeAIProvider(config, filteredFiles.length); // Get existing directory structure for context const existingStructure = await this.getExistingStructure(baseDirectory); // Prepare user preferences for AI const userPreferences = this.extractUserPreferences(config); try { // Call AI service with spinner const spinner = (0, ora_1.default)(`🤖 Analyzing ${filteredFiles.length} files with ${config.ai.provider} (${config.ai.model})...`).start(); const aiResponse = await this.aiProvider.analyzeFiles({ files: filteredFiles, baseDirectory, existingStructure, userPreferences }); spinner.succeed(`✅ AI analysis completed with ${aiResponse.suggestions.length} suggestions`); // Convert AI response to OrganizationSuggestion format const suggestions = this.convertToOrganizationSuggestions(aiResponse.suggestions); // Apply post-processing filters const finalSuggestions = this.postProcessSuggestions(suggestions, config); // Cache the results if we have any if (finalSuggestions.length > 0 && useCache) { await this.cache.cacheSuggestions(baseDirectory, filteredFiles, finalSuggestions, configHash); } return finalSuggestions; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); (0, ora_1.default)().fail(`❌ AI analysis failed: ${errorMessage}`); console.log('🔄 Falling back to rule-based organization'); return this.fallbackToRuleBasedOrganization(filteredFiles); } } async initializeAIProvider(config, fileCount = 1) { // Calculate appropriate token limit based on file count // Each file suggestion needs ~250-300 tokens, plus overhead const baseTokens = config.ai.maxTokens || 1000; const tokensPerFile = 300; const overhead = 500; const calculatedTokens = Math.max(baseTokens, (fileCount * tokensPerFile) + overhead); // Cap at reasonable limits for each provider const maxTokens = config.ai.provider === 'anthropic' ? Math.min(calculatedTokens, 4000) : Math.min(calculatedTokens, 4000); if (calculatedTokens > baseTokens) { console.log(`📈 Increasing token limit from ${baseTokens} to ${maxTokens} for ${fileCount} files`); } const aiConfig = { apiKey: config.ai.apiKey, model: config.ai.model, maxTokens: maxTokens, temperature: config.ai.temperature, timeout: config.ai.timeout }; 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}`); } } filterFiles(files, config) { return files.filter(file => { // Check include patterns const includePatterns = config.organization.includePatterns; const excludePatterns = config.organization.excludePatterns; // Check if file matches any exclude pattern const isExcluded = excludePatterns.some(pattern => { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(file.name); }); if (isExcluded) return false; // Check if file matches any include pattern const isIncluded = includePatterns.some(pattern => { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(file.name); }); if (!isIncluded) return false; // Check if file type is enabled const category = this.fileScanner.getFileCategory(file); const fileTypeConfig = config.fileTypes[category]; return fileTypeConfig?.enabled !== false; }); } async getExistingStructure(baseDirectory) { try { const scanner = new file_scanner_1.FileScanner(); const allItems = await scanner.scanDirectory(baseDirectory, true); // Get unique directory paths const directories = new Set(); allItems.forEach(item => { const dir = path.dirname(item.path); const relativePath = path.relative(baseDirectory, dir); if (relativePath && relativePath !== '.') { directories.add(relativePath); } }); return Array.from(directories).sort(); } catch (error) { console.warn('Could not analyze existing directory structure:', error); return []; } } extractUserPreferences(config) { return { confidenceThreshold: config.organization.confidenceThreshold, preserveOriginalNames: config.organization.preserveOriginalNames, maxDepth: config.organization.maxDepth, fileTypePreferences: Object.entries(config.fileTypes).reduce((acc, [type, typeConfig]) => { acc[type] = { enabled: typeConfig.enabled, organizationRules: typeConfig.organizationRules, preferredNamingPattern: Object.keys(typeConfig.namingPatterns)[0] }; return acc; }, {}), customCategories: config.customCategories }; } convertToOrganizationSuggestions(aiSuggestions) { return aiSuggestions.map(suggestion => ({ file: suggestion.file, suggestedPath: suggestion.suggestedPath, reason: suggestion.reason, confidence: suggestion.confidence })); } postProcessSuggestions(suggestions, config) { return suggestions .filter(suggestion => suggestion.confidence >= config.organization.confidenceThreshold) .map(suggestion => { // Apply naming pattern preferences if configured if (config.organization.preserveOriginalNames) { const originalName = suggestion.file.name; const dir = path.dirname(suggestion.suggestedPath); suggestion.suggestedPath = path.join(dir, originalName); } return suggestion; }); } async fallbackToRuleBasedOrganization(files) { console.log('🔄 Using rule-based fallback organization'); const suggestions = []; for (const file of files) { const category = this.fileScanner.getFileCategory(file); const baseDir = path.dirname(file.path); let suggestedPath; let reason; // Simple category-based organization suggestedPath = path.join(baseDir, category, file.name); reason = `Fallback: organized by file type (${category})`; // Only suggest if the file would actually move if (suggestedPath !== file.path) { suggestions.push({ file, suggestedPath, reason, confidence: 0.6 }); } } return suggestions; } } exports.AIOrganizer = AIOrganizer; //# sourceMappingURL=ai-organizer.js.map