@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
JavaScript
/**
* 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