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

684 lines 23.3 kB
/** * Smart Merge Tool - 80% Token Reduction * * Achieves token reduction through: * 1. Structured merge status (instead of raw git merge output) * 2. Conflict-only mode (show only conflicts, not all changes) * 3. Summary mode (counts and status, not full diffs) * 4. Smart conflict resolution helpers * 5. Minimal merge history (only essential info) * * Target: 80% reduction vs full git merge/status output */ 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 SmartMergeTool { cache; tokenCounter; metrics; constructor(cache, tokenCounter, metrics) { this.cache = cache; this.tokenCounter = tokenCounter; this.metrics = metrics; } /** * Smart merge operations with structured output and conflict management */ async merge(options = {}) { const startTime = Date.now(); // Default options const opts = { cwd: options.cwd ?? process.cwd(), mode: options.mode ?? 'status', branch: options.branch ?? '', commit: options.commit ?? '', noCommit: options.noCommit ?? false, noFf: options.noFf ?? false, ffOnly: options.ffOnly ?? false, squash: options.squash ?? false, strategy: options.strategy ?? 'recursive', strategyOption: options.strategyOption ?? [], conflictsOnly: options.conflictsOnly ?? false, includeContent: options.includeContent ?? false, summaryOnly: options.summaryOnly ?? false, maxConflicts: options.maxConflicts ?? Infinity, resolveUsing: options.resolveUsing ?? 'ours', useCache: options.useCache ?? true, ttl: options.ttl ?? 60, }; try { // Verify git repository if (!this.isGitRepository(opts.cwd)) { throw new Error(`Not a git repository: ${opts.cwd}`); } // Get current branch const currentBranch = this.getCurrentBranch(opts.cwd); // Build cache key const cacheKey = this.buildCacheKey(opts); // Check cache (only for status mode) if (opts.useCache && opts.mode === 'status') { const cached = this.cache.get(cacheKey); if (cached) { const result = JSON.parse(cached); result.metadata.cacheHit = true; const duration = Date.now() - startTime; this.metrics.record({ operation: 'smart_merge', duration, inputTokens: result.metadata.tokenCount, outputTokens: 0, cachedTokens: result.metadata.originalTokenCount, savedTokens: result.metadata.tokensSaved, success: true, cacheHit: true, }); return result; } } // Perform operation based on mode let status; let mergeResult; let resultTokens; let originalTokens; switch (opts.mode) { case 'status': status = this.getMergeStatus(opts); resultTokens = this.tokenCounter.count(JSON.stringify(status)).tokens; // Estimate original tokens (full git status + diff output) if (opts.summaryOnly) { originalTokens = resultTokens * 50; // Summary vs full output } else if (opts.conflictsOnly) { originalTokens = resultTokens * 10; // Conflicts only vs full diff } else { originalTokens = resultTokens * 5; // Structured vs raw output } break; case 'merge': if (!opts.branch && !opts.commit) { throw new Error('branch or commit required for merge operation'); } mergeResult = this.performMerge(opts); resultTokens = this.tokenCounter.count(JSON.stringify(mergeResult)).tokens; originalTokens = resultTokens * 8; // Structured result vs full merge output break; case 'abort': this.abortMerge(opts.cwd); mergeResult = { success: true, merged: false, fastForward: false, conflicts: [], message: 'Merge aborted', }; resultTokens = this.tokenCounter.count(JSON.stringify(mergeResult)).tokens; originalTokens = resultTokens * 5; break; case 'continue': mergeResult = this.continueMerge(opts.cwd); resultTokens = this.tokenCounter.count(JSON.stringify(mergeResult)).tokens; originalTokens = resultTokens * 8; break; default: throw new Error(`Invalid mode: ${opts.mode}`); } const tokensSaved = originalTokens - resultTokens; const compressionRatio = resultTokens / originalTokens; // Build result const result = { success: true, mode: opts.mode, metadata: { repository: opts.cwd, currentBranch, mergeInProgress: status?.inProgress ?? false, hasConflicts: status?.hasConflicts ?? (mergeResult?.conflicts.length ?? 0) > 0, conflictCount: status?.conflictCount ?? mergeResult?.conflicts.length ?? 0, mergedCount: status?.mergedCount ?? 0, tokensSaved, tokenCount: resultTokens, originalTokenCount: originalTokens, compressionRatio, duration: 0, // Will be set below cacheHit: false, }, status, result: mergeResult, }; // Cache result (only for status mode) if (opts.useCache && opts.mode === 'status') { 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_merge', 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_merge', duration, inputTokens: 0, outputTokens: 0, cachedTokens: 0, savedTokens: 0, success: false, cacheHit: false, }); return { success: false, mode: opts.mode, metadata: { repository: opts.cwd, currentBranch: '', mergeInProgress: false, hasConflicts: false, conflictCount: 0, mergedCount: 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 current branch name */ getCurrentBranch(cwd) { try { return execSync('git branch --show-current', { cwd, encoding: 'utf-8', }).trim(); } catch { return 'HEAD'; } } /** * 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 headHash = this.getGitHash(opts.cwd, 'HEAD'); return generateCacheKey('git-merge', { head: headHash, mode: opts.mode, conflictsOnly: opts.conflictsOnly, summaryOnly: opts.summaryOnly, }); } /** * Get current merge status */ getMergeStatus(opts) { const cwd = opts.cwd; // Check if merge is in progress const inProgress = this.isMergeInProgress(cwd); if (!inProgress) { return { inProgress: false, hasConflicts: false, conflictCount: 0, mergedCount: 0, }; } // Get merge head info const mergeBranch = this.getMergeHead(cwd); // Get conflict information const conflicts = this.getConflicts(cwd, opts.includeContent); const hasConflicts = conflicts.length > 0; // Get merged files const mergedFiles = this.getMergedFiles(cwd); // Apply conflict limit const limitedConflicts = conflicts.slice(0, opts.maxConflicts); // Build status based on output mode const status = { inProgress, hasConflicts, branch: mergeBranch, conflictCount: conflicts.length, mergedCount: mergedFiles.length, }; if (!opts.summaryOnly) { if (opts.conflictsOnly) { status.conflicts = limitedConflicts; } else { status.conflicts = limitedConflicts; status.mergedFiles = mergedFiles; } } return status; } /** * Check if merge is in progress */ isMergeInProgress(cwd) { try { execSync('git rev-parse MERGE_HEAD', { cwd, stdio: 'pipe' }); return true; } catch { return false; } } /** * Get merge head branch name */ getMergeHead(cwd) { try { const mergeMsg = execSync('cat .git/MERGE_MSG', { cwd, encoding: 'utf-8', }); const match = mergeMsg.match(/Merge branch '([^']+)'/); return match ? match[1] : 'unknown'; } catch { return undefined; } } /** * Get list of conflicted files */ getConflicts(cwd, includeContent) { try { // Get unmerged files from git status const output = execSync('git diff --name-only --diff-filter=U', { cwd, encoding: 'utf-8', }); const files = output.split('\n').filter((f) => f.trim()); const conflicts = []; for (const file of files) { const conflict = { file, type: this.getConflictType(file, cwd), }; if (includeContent) { try { // Get different versions const stages = this.getConflictStages(file, cwd); conflict.base = stages.base; conflict.ours = stages.ours; conflict.theirs = stages.theirs; } catch { // Skip if can't get stages } } conflicts.push(conflict); } return conflicts; } catch { return []; } } /** * Get conflict type for a file */ getConflictType(file, cwd) { try { const output = execSync(`git ls-files -u "${file}"`, { cwd, encoding: 'utf-8', }); if (!output) return 'content'; const lines = output.split('\n').filter((l) => l.trim()); // Check if file was deleted in one branch if (lines.some((l) => l.includes('000000'))) { return 'delete'; } // Check for rename conflicts if (lines.length > 3) { return 'rename'; } return 'content'; } catch { return 'content'; } } /** * Get different versions (stages) of a conflicted file */ getConflictStages(file, cwd) { const stages = {}; try { // Stage 1 = base (common ancestor) try { stages.base = execSync(`git show :1:"${file}"`, { cwd, encoding: 'utf-8', stdio: 'pipe', }); } catch { } // Stage 2 = ours (current branch) try { stages.ours = execSync(`git show :2:"${file}"`, { cwd, encoding: 'utf-8', stdio: 'pipe', }); } catch { } // Stage 3 = theirs (merged branch) try { stages.theirs = execSync(`git show :3:"${file}"`, { cwd, encoding: 'utf-8', stdio: 'pipe', }); } catch { } } catch { } return stages; } /** * Get list of successfully merged files */ getMergedFiles(cwd) { try { const output = execSync('git diff --name-only --diff-filter=M --cached', { cwd, encoding: 'utf-8', }); return output.split('\n').filter((f) => f.trim()); } catch { return []; } } /** * Perform merge operation */ performMerge(opts) { const cwd = opts.cwd; const target = opts.branch || opts.commit; try { // Build merge command let command = 'git merge'; if (opts.noCommit) command += ' --no-commit'; if (opts.noFf) command += ' --no-ff'; if (opts.ffOnly) command += ' --ff-only'; if (opts.squash) command += ' --squash'; if (opts.strategy) command += ` --strategy=${opts.strategy}`; for (const option of opts.strategyOption) { command += ` --strategy-option=${option}`; } command += ` "${target}"`; // Execute merge const output = execSync(command, { cwd, encoding: 'utf-8', stdio: 'pipe', }); // Check if fast-forward const fastForward = output.includes('Fast-forward'); // Get merge commit info let hash; let message; if (!opts.noCommit && !opts.squash) { try { hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf-8', }).trim(); message = execSync('git log -1 --format=%s', { cwd, encoding: 'utf-8', }).trim(); } catch { } } return { success: true, merged: true, fastForward, conflicts: [], hash, message, }; } catch (error) { // Merge failed - likely due to conflicts const conflicts = this.getConflicts(cwd, false).map((c) => c.file); return { success: conflicts.length === 0, merged: false, fastForward: false, conflicts, message: error instanceof Error ? error.message : String(error), }; } } /** * Abort current merge */ abortMerge(cwd) { try { execSync('git merge --abort', { cwd, stdio: 'pipe' }); } catch (error) { throw new Error('Failed to abort merge: ' + (error instanceof Error ? error.message : String(error))); } } /** * Continue merge after resolving conflicts */ continueMerge(cwd) { try { // Check if there are still unresolved conflicts const conflicts = this.getConflicts(cwd, false); if (conflicts.length > 0) { return { success: false, merged: false, fastForward: false, conflicts: conflicts.map((c) => c.file), message: 'Unresolved conflicts remain', }; } // Commit the merge execSync('git commit --no-edit', { cwd, stdio: 'pipe' }); // Get merge commit info const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf-8', }).trim(); const message = execSync('git log -1 --format=%s', { cwd, encoding: 'utf-8', }).trim(); return { success: true, merged: true, fastForward: false, conflicts: [], hash, message, }; } catch (error) { return { success: false, merged: false, fastForward: false, conflicts: [], message: error instanceof Error ? error.message : String(error), }; } } /** * Get merge statistics */ getStats() { const mergeMetrics = this.metrics.getOperations(0, 'smart_merge'); const totalMerges = mergeMetrics.length; const cacheHits = mergeMetrics.filter((m) => m.cacheHit).length; const totalTokensSaved = mergeMetrics.reduce((sum, m) => sum + (m.savedTokens || 0), 0); const totalInputTokens = mergeMetrics.reduce((sum, m) => sum + (m.inputTokens || 0), 0); const totalOriginalTokens = totalInputTokens + totalTokensSaved; const averageReduction = totalOriginalTokens > 0 ? (totalTokensSaved / totalOriginalTokens) * 100 : 0; return { totalMerges, cacheHits, totalTokensSaved, averageReduction, }; } } /** * Get smart merge tool instance */ export function getSmartMergeTool(cache, tokenCounter, metrics) { return new SmartMergeTool(cache, tokenCounter, metrics); } /** * CLI function - Creates resources and uses factory */ export async function runSmartMerge(options = {}) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache'), 100); const tokenCounter = new TokenCounter(); const metrics = new MetricsCollector(); const tool = getSmartMergeTool(cache, tokenCounter, metrics); return tool.merge(options); } /** * MCP Tool Definition */ export const SMART_MERGE_TOOL_DEFINITION = { name: 'smart_merge', description: 'Manage git merges with 80% token reduction through structured status and conflict management', inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Working directory for git operations', }, mode: { type: 'string', enum: ['status', 'merge', 'abort', 'continue'], description: 'Operation to perform', default: 'status', }, branch: { type: 'string', description: 'Branch to merge from (for merge mode)', }, commit: { type: 'string', description: 'Specific commit to merge (for merge mode)', }, noCommit: { type: 'boolean', description: 'Do not create merge commit', default: false, }, noFf: { type: 'boolean', description: 'No fast-forward merge', default: false, }, ffOnly: { type: 'boolean', description: 'Fast-forward only', default: false, }, squash: { type: 'boolean', description: 'Squash commits', default: false, }, strategy: { type: 'string', enum: ['recursive', 'ours', 'theirs', 'octopus', 'subtree'], description: 'Merge strategy', default: 'recursive', }, conflictsOnly: { type: 'boolean', description: 'Only return conflict information', default: false, }, includeContent: { type: 'boolean', description: 'Include file content for conflicts', default: false, }, summaryOnly: { type: 'boolean', description: 'Only return counts and status', default: false, }, maxConflicts: { type: 'number', description: 'Maximum conflicts to return', }, }, }, }; //# sourceMappingURL=smart-merge.js.map