UNPKG

@ooples/token-optimizer-mcp

Version:

Intelligent context window optimization for Claude Code - store content externally via caching and compression, freeing up your context window for what matters

475 lines • 17.9 kB
/** * Smart Install Tool - Package Installation with Dependency Analysis * * Wraps package managers (npm/yarn/pnpm) to provide: * - Package manager auto-detection * - Dependency analysis and conflict detection * - Installation progress tracking * - Token-optimized output */ import { spawn } from 'child_process'; import { CacheEngine } from '../../core/cache-engine.js'; import { createHash } from 'crypto'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class SmartInstall { cache; cacheNamespace = 'smart_install'; projectRoot; constructor(cache, projectRoot) { this.cache = cache; this.projectRoot = projectRoot || process.cwd(); } /** * Run installation with smart analysis */ async run(options = {}) { const { force = false, packageManager, packages = [], dev = false, maxCacheAge = 3600, } = options; const startTime = Date.now(); // Detect package manager const detectedPm = packageManager || this.detectPackageManager(); // Check if lockfile exists BEFORE running install (for recommendations) const lockFile = detectedPm === 'npm' ? 'package-lock.json' : detectedPm === 'yarn' ? 'yarn.lock' : 'pnpm-lock.yaml'; const hadLockfileBeforeInstall = existsSync(join(this.projectRoot, lockFile)); // Generate cache key const cacheKey = this.generateCacheKey(detectedPm, packages, dev); // Check cache first (unless force mode) if (!force) { const cached = this.getCachedResult(cacheKey, maxCacheAge); if (cached) { return this.formatCachedOutput(cached); } } // Run installation const result = await this.runInstall({ packageManager: detectedPm, packages, dev, }); // Store pre-install lockfile state for recommendations result.hadLockfileBeforeInstall = hadLockfileBeforeInstall; const duration = Date.now() - startTime; result.duration = duration; // Cache the result this.cacheResult(cacheKey, result); // Generate recommendations const recommendations = this.generateRecommendations(result); // Transform to smart output return this.transformOutput(result, recommendations); } /** * Detect which package manager is in use */ detectPackageManager() { const projectRoot = this.projectRoot; // Check for lock files if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) { return 'pnpm'; } if (existsSync(join(projectRoot, 'yarn.lock'))) { return 'yarn'; } if (existsSync(join(projectRoot, 'package-lock.json'))) { return 'npm'; } // Default to npm return 'npm'; } /** * Run package installation */ async runInstall(options) { const { packageManager, packages, dev } = options; let args = []; // Build command args based on package manager if (packages.length === 0) { // Install all dependencies args = packageManager === 'yarn' ? [] : ['install']; } else { // Install specific packages if (packageManager === 'npm') { args = ['install', ...packages]; if (dev) args.push('--save-dev'); } else if (packageManager === 'yarn') { args = ['add', ...packages]; if (dev) args.push('--dev'); } else if (packageManager === 'pnpm') { args = ['add', ...packages]; if (dev) args.push('--save-dev'); } } return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; const child = spawn(packageManager, args, { cwd: this.projectRoot, shell: true, }); child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { const output = stdout + stderr; const installedPackages = this.parseInstalledPackages(output, packages); const conflicts = this.detectConflicts(output); resolve({ success: code === 0, packageManager, packagesInstalled: installedPackages, conflicts, duration: 0, // Set by caller timestamp: Date.now(), }); }); child.on('error', (err) => { reject(err); }); }); } /** * Parse installed packages from output */ parseInstalledPackages(_output, requestedPackages) { const packages = []; // Parse package.json to get actual versions const packageJsonPath = join(this.projectRoot, 'package.json'); if (existsSync(packageJsonPath)) { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); // If specific packages requested, use those if (requestedPackages.length > 0) { for (const pkg of requestedPackages) { const [name, version] = pkg.split('@'); const actualVersion = packageJson.dependencies?.[name] || packageJson.devDependencies?.[name] || version || 'latest'; const type = packageJson.devDependencies?.[name] ? 'devDependency' : 'dependency'; packages.push({ name, version: actualVersion, type }); } } else { // All dependencies for (const [name, version] of Object.entries(packageJson.dependencies || {})) { packages.push({ name, version: version, type: 'dependency', }); } for (const [name, version] of Object.entries(packageJson.devDependencies || {})) { packages.push({ name, version: version, type: 'devDependency', }); } } } return packages; } /** * Detect dependency conflicts */ detectConflicts(output) { const conflicts = []; const lines = output.split('\n'); for (const line of lines) { // npm: WARN ... requires ... but will install ... const npmMatch = line.match(/WARN.*?(\S+).*?requires.*?(\S+).*?will install.*?(\S+)/); if (npmMatch) { conflicts.push({ package: npmMatch[1], requested: npmMatch[2], installed: npmMatch[3], severity: 'warning', }); } // yarn/pnpm: warning ... has unmet peer dependency ... const peerMatch = line.match(/warning.*?(\S+).*?unmet peer dependency.*?(\S+)@(\S+)/); if (peerMatch) { conflicts.push({ package: peerMatch[1], requested: peerMatch[3], installed: 'not installed', severity: 'warning', }); } } return conflicts; } /** * Generate installation recommendations */ generateRecommendations(result) { const recommendations = []; // Check for conflicts if (result.conflicts.length > 0) { recommendations.push({ type: 'compatibility', message: `Found ${result.conflicts.length} dependency conflicts. Run 'npm ls' to investigate.`, impact: 'high', }); } // Check for lockfile (use pre-install state to avoid false positives) const lockFile = result.packageManager === 'npm' ? 'package-lock.json' : result.packageManager === 'yarn' ? 'yarn.lock' : 'pnpm-lock.yaml'; const hadLockfile = result.hadLockfileBeforeInstall; if (hadLockfile === false || (!hadLockfile && !existsSync(join(this.projectRoot, lockFile)))) { recommendations.push({ type: 'security', message: `Missing ${lockFile}. Commit it for reproducible builds.`, impact: 'high', }); } // Performance: suggest pnpm for large projects if (result.packagesInstalled.length > 100 && result.packageManager !== 'pnpm') { recommendations.push({ type: 'performance', message: 'Consider using pnpm for faster installs on large projects.', impact: 'medium', }); } return recommendations; } /** * Generate cache key */ generateCacheKey(packageManager, packages, dev) { const packageJsonPath = join(this.projectRoot, 'package.json'); const packageJsonHash = existsSync(packageJsonPath) ? createHash('md5').update(readFileSync(packageJsonPath)).digest('hex') : 'no-package-json'; const key = `${packageManager}:${packages.join(',')}:${dev}:${packageJsonHash}`; return createHash('md5').update(key).digest('hex'); } /** * Get cached result */ getCachedResult(key, maxAge) { const cached = this.cache.get(this.cacheNamespace + ':' + key); if (!cached) return null; try { const result = JSON.parse(cached); const age = (Date.now() - result.cachedAt) / 1000; if (age <= maxAge) { return result; } } catch (err) { return null; } return null; } /** * Cache result */ cacheResult(key, result) { const cacheData = { ...result, cachedAt: Date.now() }; const dataToCache = JSON.stringify(cacheData); const originalSize = this.estimateOriginalOutputSize(result); const compactSize = dataToCache.length; this.cache.set(this.cacheNamespace + ':' + key, dataToCache, originalSize, compactSize); } /** * Transform to smart output */ transformOutput(result, recommendations, fromCache = false) { const conflicts = result.conflicts.map((c) => ({ package: c.package, requested: c.requested, installed: c.installed, severity: c.severity, resolution: c.severity === 'error' ? 'Must resolve before installation' : 'Consider upgrading or adding peer dependency', })); const packages = result.packagesInstalled.map((p) => ({ name: p.name, version: p.version, type: p.type, })); const originalSize = this.estimateOriginalOutputSize(result); const compactSize = this.estimateCompactSize(result); return { summary: { success: result.success, packageManager: result.packageManager, packagesInstalled: result.packagesInstalled.length, conflictsFound: result.conflicts.length, duration: result.duration, fromCache, }, packages: packages.slice(0, 20), // Limit to 20 for output conflicts, recommendations, metrics: { originalTokens: Math.ceil(originalSize / 4), compactedTokens: Math.ceil(compactSize / 4), reductionPercentage: Math.round(((originalSize - compactSize) / originalSize) * 100), }, }; } /** * Format cached output */ formatCachedOutput(result) { const recommendations = this.generateRecommendations(result); return this.transformOutput(result, recommendations, true); } /** * Estimate original output size (full npm install output) */ estimateOriginalOutputSize(result) { // Estimate: each package line ~100 chars const packageSize = result.packagesInstalled.length * 100; // Plus progress bars and verbose output ~2000 chars return packageSize + 2000; } /** * Estimate compact output size */ estimateCompactSize(result) { const summary = { success: result.success, packagesInstalled: result.packagesInstalled.length, conflictsFound: result.conflicts.length, }; const packages = result.packagesInstalled.slice(0, 20); const conflicts = result.conflicts; return JSON.stringify({ summary, packages, conflicts }).length; } /** * Close cache connection */ close() { this.cache.close(); } } /** * Factory function for dependency injection */ export function getSmartInstall(cache, projectRoot) { return new SmartInstall(cache, projectRoot); } /** * CLI-friendly function for running smart install */ export async function runSmartInstall(options = {}) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache'), 100); const smartInstall = getSmartInstall(cache, options.projectRoot); try { const result = await smartInstall.run(options); let output = `\nšŸ“¦ Smart Install Results ${result.summary.fromCache ? '(cached)' : ''}\n`; output += `${'='.repeat(50)}\n\n`; // Summary output += `Summary:\n`; output += ` Status: ${result.summary.success ? 'āœ“ Success' : 'āœ— Failed'}\n`; output += ` Package Manager: ${result.summary.packageManager}\n`; output += ` Packages Installed: ${result.summary.packagesInstalled}\n`; output += ` Conflicts: ${result.summary.conflictsFound}\n`; output += ` Duration: ${(result.summary.duration / 1000).toFixed(2)}s\n\n`; // Packages if (result.packages.length > 0) { output += `Installed Packages (showing ${Math.min(result.packages.length, 20)}):\n`; for (const pkg of result.packages.slice(0, 20)) { output += ` • ${pkg.name}@${pkg.version} (${pkg.type})\n`; } if (result.packages.length > 20) { output += ` ... and ${result.packages.length - 20} more\n`; } output += '\n'; } // Conflicts if (result.conflicts.length > 0) { output += `Dependency Conflicts:\n`; for (const conflict of result.conflicts) { const icon = conflict.severity === 'error' ? 'šŸ”“' : 'āš ļø'; output += ` ${icon} ${conflict.package}\n`; output += ` Requested: ${conflict.requested}\n`; output += ` Installed: ${conflict.installed}\n`; output += ` Resolution: ${conflict.resolution}\n`; } output += '\n'; } // Recommendations if (result.recommendations.length > 0) { output += `Recommendations:\n`; for (const rec of result.recommendations) { const icon = rec.impact === 'high' ? 'šŸ”“' : rec.impact === 'medium' ? '🟔' : '🟢'; output += ` ${icon} [${rec.type}] ${rec.message}\n`; } output += '\n'; } // Metrics output += `Token Reduction:\n`; output += ` Original: ${result.metrics.originalTokens} tokens\n`; output += ` Compacted: ${result.metrics.compactedTokens} tokens\n`; output += ` Reduction: ${result.metrics.reductionPercentage}%\n`; return output; } finally { smartInstall.close(); } } // MCP Tool definition export const SMART_INSTALL_TOOL_DEFINITION = { name: 'smart_install', description: 'Package installation with dependency analysis, conflict detection, and smart caching for npm/yarn/pnpm', inputSchema: { type: 'object', properties: { force: { type: 'boolean', description: 'Force reinstall (ignore cache)', default: false, }, projectRoot: { type: 'string', description: 'Project root directory', }, packageManager: { type: 'string', enum: ['npm', 'yarn', 'pnpm'], description: 'Package manager to use (auto-detect if not specified)', }, packages: { type: 'array', items: { type: 'string' }, description: 'Packages to install (if empty, installs all from package.json)', }, dev: { type: 'boolean', description: 'Install as dev dependency', default: false, }, maxCacheAge: { type: 'number', description: 'Maximum cache age in seconds (default: 3600)', default: 3600, }, }, }, }; //# sourceMappingURL=smart-install.js.map