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

437 lines 15.5 kB
/** * Smart Diff Tool - 85% Token Reduction * * Achieves token reduction through: * 1. Diff-only output (only changed lines, not full files) * 2. Summary mode (counts only, not actual diffs) * 3. File filtering (specific files or patterns) * 4. Context control (configurable lines before/after changes) * 5. Git-based caching (reuse diff results based on commit hashes) * * Target: 85% reduction vs full file content for changed files */ import { execSync } from 'child_process'; import { join } from 'path'; import { homedir } from 'os'; import { CacheEngine } from '../../core/cache-engine.js'; import { TokenCounter } from '../../core/token-counter.js'; import { MetricsCollector } from '../../core/metrics.js'; import { generateCacheKey } from '../shared/hash-utils.js'; export class SmartDiffTool { cache; tokenCounter; metrics; constructor(cache, tokenCounter, metrics) { this.cache = cache; this.tokenCounter = tokenCounter; this.metrics = metrics; } /** * Smart diff with configurable output and token optimization */ async diff(options = {}) { const startTime = Date.now(); // Default options const opts = { cwd: options.cwd ?? process.cwd(), source: options.source ?? 'HEAD', target: options.target ?? '', // Empty means working directory staged: options.staged ?? false, files: options.files ?? [], filePattern: options.filePattern ?? '', summaryOnly: options.summaryOnly ?? false, contextLines: options.contextLines ?? 3, unified: options.unified ?? true, includeLineNumbers: options.includeLineNumbers ?? true, includeBinary: options.includeBinary ?? false, showRenames: options.showRenames ?? true, limit: options.limit ?? Infinity, offset: options.offset ?? 0, useCache: options.useCache ?? true, ttl: options.ttl ?? 300, }; try { // Verify git repository if (!this.isGitRepository(opts.cwd)) { throw new Error(`Not a git repository: ${opts.cwd}`); } // Build cache key const cacheKey = this.buildCacheKey(opts); // Check cache if (opts.useCache) { const cached = this.cache.get(cacheKey); if (cached) { const result = JSON.parse(cached.toString()); result.metadata.cacheHit = true; const duration = Date.now() - startTime; this.metrics.record({ operation: 'smart_diff', duration, inputTokens: result.metadata.tokenCount, outputTokens: 0, cachedTokens: result.metadata.originalTokenCount, savedTokens: result.metadata.tokensSaved, success: true, cacheHit: true, }); return result; } } // Get diff statistics first const stats = this.getDiffStats(opts); // Apply pagination const totalFiles = stats.length; const paginatedStats = stats.slice(opts.offset, opts.offset + opts.limit); const truncated = totalFiles > paginatedStats.length + opts.offset; // Build result based on mode let diffs; let resultTokens; let originalTokens; if (opts.summaryOnly) { // Summary mode: return stats only resultTokens = this.tokenCounter.count(JSON.stringify(paginatedStats)).tokens; originalTokens = resultTokens * 100; // Estimate full diff would be 100x larger } else { // Full mode: get actual diffs diffs = this.getDiffs(paginatedStats.map((s) => s.file), opts); resultTokens = this.tokenCounter.count(JSON.stringify(diffs)).tokens; originalTokens = resultTokens * 10; // Estimate full files would be 10x larger } const tokensSaved = originalTokens - resultTokens; const compressionRatio = resultTokens / originalTokens; // Calculate total additions/deletions const totalAdditions = paginatedStats.reduce((sum, s) => sum + s.additions, 0); const totalDeletions = paginatedStats.reduce((sum, s) => sum + s.deletions, 0); // Build result const result = { success: true, comparison: { source: this.formatComparison(opts.source, opts.staged), target: this.formatComparison(opts.target, opts.staged), repository: opts.cwd, }, metadata: { totalFiles, returnedCount: paginatedStats.length, truncated, totalAdditions, totalDeletions, tokensSaved, tokenCount: resultTokens, originalTokenCount: originalTokens, compressionRatio, duration: 0, // Will be set below cacheHit: false, }, stats: paginatedStats, diffs: opts.summaryOnly ? undefined : diffs, }; // Cache result if (opts.useCache) { const resultString = JSON.stringify(result); const resultSize = Buffer.from(resultString, 'utf-8').length; this.cache.set(cacheKey, resultString, resultSize, resultSize); } // Record metrics const duration = Date.now() - startTime; result.metadata.duration = duration; this.metrics.record({ operation: 'smart_diff', duration, inputTokens: resultTokens, outputTokens: 0, cachedTokens: 0, savedTokens: tokensSaved, success: true, cacheHit: false, }); return result; } catch (error) { const duration = Date.now() - startTime; this.metrics.record({ operation: 'smart_diff', duration, inputTokens: 0, outputTokens: 0, cachedTokens: 0, savedTokens: 0, success: false, cacheHit: false, }); return { success: false, comparison: { source: opts.source, target: opts.target, repository: opts.cwd, }, metadata: { totalFiles: 0, returnedCount: 0, truncated: false, totalAdditions: 0, totalDeletions: 0, tokensSaved: 0, tokenCount: 0, originalTokenCount: 0, compressionRatio: 0, duration, cacheHit: false, }, error: error instanceof Error ? error.message : String(error), }; } } /** * Check if directory is a git repository */ isGitRepository(cwd) { try { execSync('git rev-parse --git-dir', { cwd, stdio: 'pipe' }); return true; } catch { return false; } } /** * Get git commit hash */ getGitHash(cwd, ref) { try { return execSync(`git rev-parse ${ref}`, { cwd, encoding: 'utf-8', }).trim(); } catch { return ref; } } /** * Build cache key from options */ buildCacheKey(opts) { const sourceHash = this.getGitHash(opts.cwd, opts.source); const targetHash = opts.target ? this.getGitHash(opts.cwd, opts.target) : 'working'; return generateCacheKey('git-diff', { source: sourceHash, target: targetHash, staged: opts.staged, files: opts.files, pattern: opts.filePattern, summaryOnly: opts.summaryOnly, context: opts.contextLines, }); } /** * Format comparison target for display */ formatComparison(ref, staged) { if (!ref && staged) return 'staged changes'; if (!ref) return 'working directory'; return ref; } /** * Get diff statistics for files */ getDiffStats(opts) { try { // Build diff command let command = 'git diff --numstat'; if (opts.staged) { command += ' --cached'; } if (opts.showRenames) { command += ' -M'; } // Add comparison targets if (opts.target) { command += ` ${opts.source}...${opts.target}`; } else if (opts.source !== 'HEAD' || opts.staged) { command += ` ${opts.source}`; } // Add file filters if (opts.files.length > 0) { command += ' -- ' + opts.files.join(' '); } else if (opts.filePattern) { command += ` -- '${opts.filePattern}'`; } const output = execSync(command, { cwd: opts.cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, // 10MB }); return this.parseNumstat(output); } catch (error) { // If diff fails, return empty stats return []; } } /** * Parse git diff --numstat output */ parseNumstat(output) { const stats = []; const lines = output.split('\n').filter((line) => line.trim()); for (const line of lines) { const parts = line.split('\t'); if (parts.length >= 3) { const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10); const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10); const file = parts[2]; stats.push({ file, additions, deletions, changes: additions + deletions, }); } } return stats; } /** * Get actual diff content for files */ getDiffs(files, opts) { const diffs = []; for (const file of files) { try { // Build diff command for single file let command = 'git diff'; if (opts.unified) { command += ` -U${opts.contextLines}`; } if (opts.staged) { command += ' --cached'; } if (opts.showRenames) { command += ' -M'; } if (!opts.includeBinary) { command += ' --no-binary'; } // Add comparison targets if (opts.target) { command += ` ${opts.source}...${opts.target}`; } else if (opts.source !== 'HEAD' || opts.staged) { command += ` ${opts.source}`; } command += ` -- "${file}"`; const diff = execSync(command, { cwd: opts.cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, // 10MB }); if (diff.trim()) { diffs.push({ file, diff }); } } catch { // Skip files that can't be diffed continue; } } return diffs; } /** * Get diff statistics */ getStats() { const diffMetrics = this.metrics.getOperations(0, 'smart_diff'); const totalDiffs = diffMetrics.length; const cacheHits = diffMetrics.filter((m) => m.cacheHit).length; const totalTokensSaved = diffMetrics.reduce((sum, m) => sum + (m.savedTokens || 0), 0); const totalInputTokens = diffMetrics.reduce((sum, m) => sum + (m.inputTokens || 0), 0); const totalOriginalTokens = totalInputTokens + totalTokensSaved; const averageReduction = totalOriginalTokens > 0 ? (totalTokensSaved / totalOriginalTokens) * 100 : 0; return { totalDiffs, cacheHits, totalTokensSaved, averageReduction, }; } } /** * Get smart diff tool instance */ export function getSmartDiffTool(cache, tokenCounter, metrics) { return new SmartDiffTool(cache, tokenCounter, metrics); } /** * CLI function - Creates resources and uses factory */ export async function runSmartDiff(options = {}) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache', 'cache.db'), 100); const tokenCounter = new TokenCounter(); const metrics = new MetricsCollector(); const tool = getSmartDiffTool(cache, tokenCounter, metrics); return tool.diff(options); } /** * MCP Tool Definition */ export const SMART_DIFF_TOOL_DEFINITION = { name: 'smart_diff', description: 'Get git diffs with 85% token reduction through diff-only output and smart filtering', inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Working directory for git operations', }, source: { type: 'string', description: 'Source commit/branch to compare from (default: HEAD)', default: 'HEAD', }, target: { type: 'string', description: 'Target commit/branch to compare to (default: working directory)', }, staged: { type: 'boolean', description: 'Diff staged changes only', default: false, }, files: { type: 'array', items: { type: 'string' }, description: 'Specific files to diff', }, filePattern: { type: 'string', description: 'Pattern to filter files (e.g., "*.ts")', }, summaryOnly: { type: 'boolean', description: 'Only return statistics, not diff content', default: false, }, contextLines: { type: 'number', description: 'Lines of context around changes', default: 3, }, limit: { type: 'number', description: 'Maximum number of files to diff', }, }, }, }; //# sourceMappingURL=smart-diff.js.map