@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
460 lines • 17 kB
JavaScript
/**
* Smart Lint Tool - 75% Token Reduction
*
* Wraps ESLint to provide:
* - Cached lint results per file
* - Show only new issues
* - Auto-fix suggestions
* - Ignore previously acknowledged issues
*/
import { spawn } from 'child_process';
import { CacheEngine } from '../../core/cache-engine.js';
import { TokenCounter } from '../../core/token-counter.js';
import { MetricsCollector } from '../../core/metrics.js';
import { createHash } from 'crypto';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
export class SmartLint {
cache;
cacheNamespace = 'smart_lint';
projectRoot;
ignoredIssuesKey = 'ignored_issues';
constructor(cache, _tokenCounter, _metrics, projectRoot) {
this.cache = cache;
this.projectRoot = projectRoot || process.cwd();
}
/**
* Run lint with smart caching and output reduction
*/
async run(options = {}) {
const { files = 'src', force = false, fix = false, onlyNew = true, includeIgnored = false, maxCacheAge = 3600, } = options;
// Generate cache key
const filePatterns = Array.isArray(files) ? files : [files];
const cacheKey = await this.generateCacheKey(filePatterns);
// Check cache first (unless force)
if (!force) {
const cached = this.getCachedResult(cacheKey, maxCacheAge);
if (cached) {
return this.formatCachedOutput(cached, onlyNew);
}
}
// Run ESLint
const result = await this.runEslint({
files: filePatterns,
fix,
});
// Cache the result
this.cacheResult(cacheKey, result);
// Transform to smart output
return this.transformOutput(result, onlyNew, includeIgnored);
}
/**
* Run ESLint and capture results
*/
async runEslint(options) {
const args = [...options.files, '--format=json'];
if (options.fix) {
args.push('--fix');
}
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const eslint = spawn('npx', ['eslint', ...args], {
cwd: this.projectRoot,
shell: true,
});
eslint.stdout.on('data', (data) => {
stdout += data.toString();
});
eslint.stderr.on('data', (data) => {
stderr += data.toString();
});
eslint.on('close', (_code) => {
try {
// ESLint outputs JSON even on errors
const result = JSON.parse(stdout);
const output = {
results: result,
errorCount: result.reduce((sum, r) => sum + r.errorCount, 0),
warningCount: result.reduce((sum, r) => sum + r.warningCount, 0),
fixableErrorCount: result.reduce((sum, r) => sum + r.fixableErrorCount, 0),
fixableWarningCount: result.reduce((sum, r) => sum + r.fixableWarningCount, 0),
};
resolve(output);
}
catch (err) {
reject(new Error(`Failed to parse ESLint output: ${stderr || stdout}`));
}
});
eslint.on('error', (err) => {
reject(err);
});
});
}
/**
* Generate cache key based on source files
*/
async generateCacheKey(files) {
const hash = createHash('sha256');
hash.update(this.cacheNamespace);
hash.update(files.join(':'));
// Hash eslint config
const eslintConfigPath = join(this.projectRoot, 'eslint.config.js');
if (existsSync(eslintConfigPath)) {
const content = readFileSync(eslintConfigPath, 'utf-8');
hash.update(content);
}
// Hash package.json for dependency changes
const packageJsonPath = join(this.projectRoot, 'package.json');
if (existsSync(packageJsonPath)) {
const packageJson = readFileSync(packageJsonPath, 'utf-8');
// Only hash eslint-related dependencies
const pkg = JSON.parse(packageJson);
const eslintDeps = Object.keys(pkg.devDependencies || {})
.filter((dep) => dep.includes('eslint'))
.sort()
.map((dep) => `${dep}:${pkg.devDependencies[dep]}`)
.join(',');
hash.update(eslintDeps);
}
return `${this.cacheNamespace}:${hash.digest('hex')}`;
}
/**
* Get cached result if available and fresh
*/
getCachedResult(key, maxAge) {
const cached = this.cache.get(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 lint result
*/
cacheResult(key, result) {
const toCache = {
...result,
cachedAt: Date.now(),
};
const dataToCache = JSON.stringify(toCache);
const originalSize = this.estimateOriginalOutputSize(result);
const compactSize = dataToCache.length;
this.cache.set(key, dataToCache, originalSize, compactSize);
}
/**
* Get previously ignored issues
*/
getIgnoredIssues() {
const cached = this.cache.get(this.ignoredIssuesKey);
if (!cached) {
return new Set();
}
try {
const ignored = JSON.parse(cached);
return new Set(ignored);
}
catch (err) {
return new Set();
}
}
/**
* Generate issue key for tracking
*/
generateIssueKey(file, line, ruleId) {
return `${file}:${line}:${ruleId}`;
}
/**
* Compare with previous run to detect new issues
*/
detectNewIssues(_current) {
// In a real implementation, we'd compare with previous cached run
// For now, return empty array (all issues are "new")
return [];
}
/**
* Transform full lint output to smart output
*/
transformOutput(result, _onlyNew, includeIgnored, fromCache = false) {
// Get ignored issues
const ignoredSet = this.getIgnoredIssues();
// Group issues by rule
const issuesByRule = new Map();
for (const fileResult of result.results) {
for (const message of fileResult.messages) {
const key = this.generateIssueKey(fileResult.filePath, message.line, message.ruleId);
// Skip ignored issues unless requested
if (!includeIgnored && ignoredSet.has(key)) {
continue;
}
if (!issuesByRule.has(message.ruleId)) {
issuesByRule.set(message.ruleId, {
severity: message.severity === 2 ? 'error' : 'warning',
fixable: !!message.fix,
occurrences: [],
});
}
const rule = issuesByRule.get(message.ruleId);
rule.occurrences.push({
file: fileResult.filePath,
line: message.line,
column: message.column,
message: message.message,
});
}
}
// Group occurrences by file
const issues = Array.from(issuesByRule.entries()).map(([ruleId, data]) => {
const fileMap = new Map();
for (const occ of data.occurrences) {
if (!fileMap.has(occ.file)) {
fileMap.set(occ.file, []);
}
fileMap.get(occ.file).push({
line: occ.line,
column: occ.column,
message: occ.message,
});
}
return {
severity: data.severity,
ruleId,
count: data.occurrences.length,
fixable: data.fixable,
files: Array.from(fileMap.entries()).map(([path, locations]) => ({
path,
locations,
})),
};
});
// Sort by count (most common first)
issues.sort((a, b) => b.count - a.count);
// Generate auto-fix suggestions
const autoFixSuggestions = issues
.filter((issue) => issue.fixable)
.map((issue) => ({
ruleId: issue.ruleId,
count: issue.count,
impact: this.estimateFixImpact(issue.count, issue.severity),
}))
.sort((a, b) => {
const impactOrder = { high: 3, medium: 2, low: 1 };
return impactOrder[b.impact] - impactOrder[a.impact];
});
// Detect new issues
const newIssues = this.detectNewIssues(result);
const originalSize = this.estimateOriginalOutputSize(result);
const compactSize = this.estimateCompactSize(result);
return {
summary: {
totalFiles: result.results.length,
errorCount: result.errorCount,
warningCount: result.warningCount,
fixableCount: result.fixableErrorCount + result.fixableWarningCount,
newIssuesCount: newIssues.length,
fromCache,
},
issues,
autoFixSuggestions,
newIssues,
_metrics: {
originalTokens: Math.ceil(originalSize / 4),
compactedTokens: Math.ceil(compactSize / 4),
reductionPercentage: Math.round(((originalSize - compactSize) / originalSize) * 100),
},
};
}
/**
* Estimate fix impact
*/
estimateFixImpact(count, severity) {
if (severity === 'error' || count > 20) {
return 'high';
}
if (count > 5) {
return 'medium';
}
return 'low';
}
/**
* Format cached output
*/
formatCachedOutput(result, onlyNew) {
return this.transformOutput(result, onlyNew, false, true);
}
/**
* Estimate original output size (full eslint output)
*/
estimateOriginalOutputSize(result) {
// Estimate: each message is ~150 chars in full output
const messageCount = result.results.reduce((sum, r) => sum + r.messages.length, 0);
return messageCount * 150 + 500; // Plus header/footer
}
/**
* Estimate compact output size
*/
estimateCompactSize(result) {
const summary = {
errorCount: result.errorCount,
warningCount: result.warningCount,
};
// Only include top 10 rules
const topRules = result.results
.flatMap((r) => r.messages)
.slice(0, 10)
.map((m) => ({ ruleId: m.ruleId, message: m.message }));
return JSON.stringify({ summary, topRules }).length;
}
/**
* Close cache connection
*/
close() {
this.cache.close();
}
}
/**
* Factory function for creating SmartLint with shared resources (for benchmarks)
*/
export function getSmartLintTool(cache, tokenCounter, metrics, projectRoot) {
return new SmartLint(cache, tokenCounter, metrics, projectRoot);
}
/**
* CLI-friendly function for running smart lint
*/
export async function runSmartLint(options = {}) {
const cacheDir = join(homedir(), '.hypercontext', 'cache');
const cache = new CacheEngine(cacheDir, 100);
const tokenCounter = new TokenCounter();
const metrics = new MetricsCollector();
const smartLint = new SmartLint(cache, tokenCounter, metrics, options.projectRoot);
try {
const result = await smartLint.run(options);
let output = `\n🔠Smart Lint Results ${result.summary.fromCache ? '(cached)' : ''}\n`;
output += `${'='.repeat(50)}\n\n`;
// Summary
output += `Summary:\n`;
output += ` Files: ${result.summary.totalFiles}\n`;
output += ` Errors: ${result.summary.errorCount}\n`;
output += ` Warnings: ${result.summary.warningCount}\n`;
output += ` Fixable: ${result.summary.fixableCount}\n`;
if (result.summary.newIssuesCount > 0) {
output += ` New Issues: ${result.summary.newIssuesCount}\n`;
}
output += '\n';
// Issues by rule (top 10)
if (result.issues.length > 0) {
output += `Issues by Rule:\n`;
for (const issue of result.issues.slice(0, 10)) {
const icon = issue.severity === 'error' ? '✗' : '⚠';
const fixIcon = issue.fixable ? '🔧' : '';
output += ` ${icon} ${issue.ruleId} (${issue.count}) ${fixIcon}\n`;
// Show first file as example
if (issue.files.length > 0) {
const firstFile = issue.files[0];
const firstLoc = firstFile.locations[0];
output += ` ${firstFile.path}:${firstLoc.line}:${firstLoc.column}\n`;
output += ` ${firstLoc.message}\n`;
if (issue.files.length > 1 || firstFile.locations.length > 1) {
const totalOccurrences = issue.files.reduce((sum, f) => sum + f.locations.length, 0);
output += ` ... ${totalOccurrences - 1} more occurrences\n`;
}
}
}
if (result.issues.length > 10) {
output += ` ... and ${result.issues.length - 10} more rules\n`;
}
output += '\n';
}
// Auto-fix suggestions
if (result.autoFixSuggestions.length > 0) {
output += `Auto-Fix Suggestions:\n`;
for (const suggestion of result.autoFixSuggestions.slice(0, 5)) {
const icon = suggestion.impact === 'high'
? '🔴'
: suggestion.impact === 'medium'
? '🟡'
: '🟢';
output += ` ${icon} ${suggestion.ruleId} (${suggestion.count} occurrences)\n`;
output += ` Run: npx eslint --fix --rule "${suggestion.ruleId}"\n`;
}
output += '\n';
}
// New issues
if (result.newIssues.length > 0) {
output += `New Issues:\n`;
for (const issue of result.newIssues.slice(0, 5)) {
output += ` • ${issue.file}:${issue.location}\n`;
output += ` [${issue.ruleId}] ${issue.message}\n`;
}
if (result.newIssues.length > 5) {
output += ` ... and ${result.newIssues.length - 5} more\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 {
smartLint.close();
}
}
// MCP Tool definition
export const SMART_LINT_TOOL_DEFINITION = {
name: 'smart_lint',
description: 'Run ESLint with intelligent caching, incremental analysis, and auto-fix suggestions',
inputSchema: {
type: 'object',
properties: {
files: {
type: ['string', 'array'],
description: 'Files or pattern to lint',
items: { type: 'string' },
},
force: {
type: 'boolean',
description: 'Force full lint (ignore cache)',
default: false,
},
fix: {
type: 'boolean',
description: 'Auto-fix issues',
default: false,
},
projectRoot: {
type: 'string',
description: 'Project root directory',
},
onlyNew: {
type: 'boolean',
description: 'Show only new issues since last run',
default: false,
},
includeIgnored: {
type: 'boolean',
description: 'Include previously ignored issues',
default: false,
},
maxCacheAge: {
type: 'number',
description: 'Maximum cache age in seconds (default: 3600)',
default: 3600,
},
},
},
};
//# sourceMappingURL=smart-lint.js.map