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

556 lines • 19.4 kB
/** * Smart Docker Tool - Docker Operations with Intelligence * * Wraps Docker commands to provide: * - Build, run, stop, logs operations * - Image layer analysis * - Resource usage 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 SmartDocker { cache; cacheNamespace = 'smart_docker'; projectRoot; constructor(cache, projectRoot) { this.cache = cache; this.projectRoot = projectRoot || process.cwd(); } /** * Run Docker operation with smart analysis */ async run(options) { const { operation, force = false, maxCacheAge = 3600 } = options; const startTime = Date.now(); // Generate cache key const cacheKey = this.generateCacheKey(operation, options); // Check cache first (unless force mode or logs operations) // Note: ps operations are cached with shorter TTL since they can change if (!force && operation !== 'logs') { const cached = this.getCachedResult(cacheKey, maxCacheAge); if (cached) { return this.formatCachedOutput(cached); } } // Run Docker operation const result = await this.runDockerOperation(options); const duration = Date.now() - startTime; result.duration = duration; // Cache the result (except logs which are dynamic) // ps operations use shorter cache TTL (60 seconds) compared to builds (3600 seconds) if (operation !== 'logs') { const cacheTTL = operation === 'ps' ? 60 : 3600; this.cacheResult(cacheKey, result, cacheTTL); } // Generate suggestions const suggestions = this.generateSuggestions(result, options); // Transform to smart output return this.transformOutput(result, suggestions); } /** * Run Docker operation */ async runDockerOperation(options) { const { operation } = options; switch (operation) { case 'build': return this.dockerBuild(options); case 'run': return this.dockerRun(options); case 'stop': return this.dockerStop(options); case 'logs': return this.dockerLogs(options); case 'ps': return this.dockerPs(options); default: throw new Error(`Unknown operation: ${operation}`); } } /** * Docker build operation */ async dockerBuild(options) { const { dockerfile = 'Dockerfile', imageName = 'app:latest', context = '.', } = options; const args = ['build', '-f', dockerfile, '-t', imageName, context]; return this.execDocker(args, 'build'); } /** * Docker run operation */ async dockerRun(options) { const { imageName = 'app:latest', containerName = 'app-container', ports = [], env = {}, } = options; const args = ['run', '-d', '--name', containerName]; // Add port mappings for (const port of ports) { args.push('-p', port); } // Add environment variables for (const [key, value] of Object.entries(env)) { args.push('-e', `${key}=${value}`); } args.push(imageName); return this.execDocker(args, 'run'); } /** * Docker stop operation */ async dockerStop(options) { const { containerName = 'app-container' } = options; const args = ['stop', containerName]; return this.execDocker(args, 'stop'); } /** * Docker logs operation */ async dockerLogs(options) { const { containerName = 'app-container', follow = false, tail = 100, } = options; const args = ['logs']; if (follow) { args.push('-f'); } if (tail) { args.push('--tail', tail.toString()); } args.push(containerName); return this.execDocker(args, 'logs'); } /** * Docker ps operation */ async dockerPs(_options) { const args = [ 'ps', '-a', '--format', '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}', ]; return this.execDocker(args, 'ps'); } /** * Execute Docker command */ async execDocker(args, operation) { return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; const docker = spawn('docker', args, { cwd: this.projectRoot, shell: true, }); docker.stdout.on('data', (data) => { stdout += data.toString(); }); docker.stderr.on('data', (data) => { stderr += data.toString(); }); docker.on('close', (code) => { const output = stdout + stderr; const result = { success: code === 0, operation, duration: 0, // Set by caller timestamp: Date.now(), }; // Parse output based on operation if (operation === 'ps') { result.containers = this.parseContainers(stdout); } else if (operation === 'logs') { result.logs = this.parseLogs(stdout); } else if (operation === 'build') { result.buildLayers = this.countBuildLayers(output); } resolve(result); }); docker.on('error', (err) => { reject(err); }); }); } /** * Parse container list */ parseContainers(output) { const containers = []; const lines = output.split('\n').filter((l) => l.trim()); for (const line of lines) { const [id, name, image, status, ports] = line.split('|'); if (id && name) { containers.push({ id: id.substring(0, 12), name, image, status, ports: ports ? ports.split(',').map((p) => p.trim()) : [], }); } } return containers; } /** * Parse log output */ parseLogs(output) { return output .split('\n') .filter((l) => l.trim()) .slice(-100); // Keep last 100 lines } /** * Count build layers */ countBuildLayers(output) { const stepMatches = output.match(/Step \d+\/\d+/g); return stepMatches ? stepMatches.length : 0; } /** * Generate optimization suggestions */ generateSuggestions(result, options) { const suggestions = []; // Check for Dockerfile best practices const dockerfilePath = join(this.projectRoot, options.dockerfile || 'Dockerfile'); if (existsSync(dockerfilePath)) { const dockerfileContent = readFileSync(dockerfilePath, 'utf-8'); // Check for .dockerignore if (!existsSync(join(this.projectRoot, '.dockerignore'))) { suggestions.push({ type: 'size', message: 'Add .dockerignore to reduce build context size.', impact: 'medium', }); } // Check for multi-stage builds // Count layers from Dockerfile if not available from build operation const layerCount = result.buildLayers || this.countDockerfileLayers(dockerfileContent); if (!dockerfileContent.includes('AS ') && layerCount > 10) { suggestions.push({ type: 'size', message: 'Consider using multi-stage builds to reduce image size.', impact: 'high', }); } // Check for latest tag if (dockerfileContent.includes('FROM ') && dockerfileContent.includes(':latest')) { suggestions.push({ type: 'security', message: 'Avoid using :latest tag in FROM statements for reproducible builds.', impact: 'high', }); } // Check for root user if (!dockerfileContent.includes('USER ')) { suggestions.push({ type: 'security', message: 'Specify a non-root USER in Dockerfile for better security.', impact: 'medium', }); } } return suggestions; } /** * Generate cache key */ generateCacheKey(operation, options) { const keyParts = [ operation, options.imageName || '', options.containerName || '', options.dockerfile || '', ]; // Include Dockerfile hash for build operations if (operation === 'build') { const dockerfilePath = join(this.projectRoot, options.dockerfile || 'Dockerfile'); if (existsSync(dockerfilePath)) { const hash = createHash('md5') .update(readFileSync(dockerfilePath)) .digest('hex'); keyParts.push(hash); } } return createHash('md5').update(keyParts.join(':')).digest('hex'); } /** * Count layers in a Dockerfile */ countDockerfileLayers(content) { const layerCommands = ['RUN', 'COPY', 'ADD', 'WORKDIR', 'ENV']; const lines = content.split('\n'); let count = 0; for (const line of lines) { const trimmed = line.trim(); if (layerCommands.some((cmd) => trimmed.startsWith(cmd + ' '))) { count++; } } return count; } /** * 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, _ttl = 3600) { 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, suggestions, fromCache = false) { const output = { summary: { success: result.success, operation: result.operation, duration: result.duration, fromCache, }, suggestions, metrics: { originalTokens: 0, compactedTokens: 0, reductionPercentage: 0, }, }; // Add operation-specific data if (result.containers) { output.containers = result.containers.map((c) => ({ id: c.id, name: c.name, image: c.image, status: c.status, ports: c.ports, })); } if (result.logs) { output.logs = result.logs.map((line) => { const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)/); const levelMatch = line.match(/\[(ERROR|WARN|INFO|DEBUG)\]/); return { timestamp: timestampMatch ? timestampMatch[1] : 'unknown', level: levelMatch ? levelMatch[1] : 'info', message: line, }; }); } if (result.buildLayers) { output.buildInfo = { layers: result.buildLayers, cacheHits: 0, // TODO: Parse from build output totalSize: 'unknown', // TODO: Get from docker images }; } // Calculate metrics const originalSize = this.estimateOriginalOutputSize(result); const compactSize = this.estimateCompactSize(output); output.metrics = { originalTokens: Math.ceil(originalSize / 4), compactedTokens: Math.ceil(compactSize / 4), reductionPercentage: Math.round(((originalSize - compactSize) / originalSize) * 100), }; return output; } /** * Format cached output */ formatCachedOutput(result) { return this.transformOutput(result, [], true); } /** * Estimate original output size */ estimateOriginalOutputSize(result) { // Estimate: Docker verbose output can be very large let size = 1000; // Base if (result.containers) { size += result.containers.length * 200; } if (result.logs) { size += result.logs.reduce((sum, log) => sum + log.length, 0); } if (result.buildLayers) { size += result.buildLayers * 150; // Each layer output } return size; } /** * Estimate compact output size */ estimateCompactSize(output) { return JSON.stringify(output).length; } /** * Close cache connection */ close() { this.cache.close(); } } /** * Factory function for dependency injection */ export function getSmartDocker(cache, projectRoot) { return new SmartDocker(cache, projectRoot); } /** * CLI-friendly function for running smart docker */ export async function runSmartDocker(options) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache'), 100); const smartDocker = getSmartDocker(cache, options.projectRoot); try { const result = await smartDocker.run(options); let output = `\n🐳 Smart Docker Results ${result.summary.fromCache ? '(cached)' : ''}\n`; output += `${'='.repeat(50)}\n\n`; // Summary output += `Summary:\n`; output += ` Operation: ${result.summary.operation}\n`; output += ` Status: ${result.summary.success ? 'āœ“ Success' : 'āœ— Failed'}\n`; output += ` Duration: ${(result.summary.duration / 1000).toFixed(2)}s\n\n`; // Containers if (result.containers && result.containers.length > 0) { output += `Containers:\n`; for (const container of result.containers) { output += ` • ${container.name} (${container.id})\n`; output += ` Image: ${container.image}\n`; output += ` Status: ${container.status}\n`; if (container.ports.length > 0) { output += ` Ports: ${container.ports.join(', ')}\n`; } } output += '\n'; } // Build info if (result.buildInfo) { output += `Build Information:\n`; output += ` Layers: ${result.buildInfo.layers}\n`; output += ` Cache Hits: ${result.buildInfo.cacheHits}\n`; output += ` Total Size: ${result.buildInfo.totalSize}\n\n`; } // Logs if (result.logs && result.logs.length > 0) { output += `Recent Logs (${result.logs.length} entries):\n`; for (const log of result.logs.slice(-20)) { const icon = log.level === 'ERROR' ? 'šŸ”“' : log.level === 'WARN' ? 'āš ļø' : 'ā„¹ļø'; output += ` ${icon} ${log.message}\n`; } output += '\n'; } // Suggestions if (result.suggestions.length > 0) { output += `Optimization Suggestions:\n`; for (const suggestion of result.suggestions) { const icon = suggestion.impact === 'high' ? 'šŸ”“' : suggestion.impact === 'medium' ? '🟔' : '🟢'; output += ` ${icon} [${suggestion.type}] ${suggestion.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 { smartDocker.close(); } } // MCP Tool definition export const SMART_DOCKER_TOOL_DEFINITION = { name: 'smart_docker', description: 'Docker operations with build/run/stop/logs support, image layer analysis, and optimization suggestions', inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['build', 'run', 'stop', 'logs', 'ps'], description: 'Docker operation to perform', }, force: { type: 'boolean', description: 'Force operation (ignore cache)', default: false, }, projectRoot: { type: 'string', description: 'Project root directory', }, dockerfile: { type: 'string', description: 'Dockerfile path', }, imageName: { type: 'string', description: 'Image name for build/run', }, containerName: { type: 'string', description: 'Container name for run/stop/logs', }, context: { type: 'string', description: 'Build context directory', }, ports: { type: 'array', items: { type: 'string' }, description: "Port mappings for run (e.g., ['8080:80', '443:443'])", }, env: { type: 'object', description: 'Environment variables for run', }, follow: { type: 'boolean', description: 'Follow logs (tail mode)', default: false, }, tail: { type: 'number', description: 'Number of log lines to show', }, maxCacheAge: { type: 'number', description: 'Maximum cache age in seconds (default: 3600)', default: 3600, }, }, required: ['operation'], }, }; //# sourceMappingURL=smart-docker.js.map