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