organ-ai-zer
Version:
AI-powered file organizer CLI tool
246 lines • 11 kB
JavaScript
;
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