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

368 lines 14.5 kB
/** * Smart Glob Tool - 75% Token Reduction * * Achieves token reduction through: * 1. Path-only results (no file content unless requested) * 2. Smart pagination (limit results, return counts) * 3. Cached pattern results (reuse glob results) * 4. Metadata filtering (filter before returning) * 5. Intelligent sorting (most relevant first) * * Target: 75% reduction vs listing all files with content */ import { globSync } from 'glob'; import { statSync, readFileSync } from 'fs'; import { relative, basename, extname, 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'; import { detectFileType } from '../shared/syntax-utils.js'; export class SmartGlobTool { cache; tokenCounter; metrics; constructor(cache, tokenCounter, metrics) { this.cache = cache; this.tokenCounter = tokenCounter; this.metrics = metrics; } /** * Smart glob with filtering, pagination, and minimal token output */ async glob(pattern, options = {}) { const startTime = Date.now(); // Default options const opts = { cwd: options.cwd ?? process.cwd(), absolute: options.absolute ?? false, ignore: options.ignore ?? [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', ], onlyFiles: options.onlyFiles ?? true, onlyDirectories: options.onlyDirectories ?? false, extensions: options.extensions ?? [], excludeExtensions: options.excludeExtensions ?? [], minSize: options.minSize ?? 0, maxSize: options.maxSize ?? Infinity, modifiedAfter: options.modifiedAfter ?? new Date(0), modifiedBefore: options.modifiedBefore ?? new Date(8640000000000000), // Max date includeMetadata: options.includeMetadata ?? false, includeContent: options.includeContent ?? false, maxContentSize: options.maxContentSize ?? 10240, // 10KB limit: options.limit ?? Infinity, offset: options.offset ?? 0, sortBy: options.sortBy ?? 'path', sortOrder: options.sortOrder ?? 'asc', useCache: options.useCache ?? true, ttl: options.ttl ?? 300, }; try { // Check cache first const cacheKey = generateCacheKey('glob', { pattern, options: opts }); 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_glob', duration, inputTokens: result.metadata.tokenCount, outputTokens: 0, cachedTokens: result.metadata.originalTokenCount, savedTokens: result.metadata.tokensSaved, success: true, cacheHit: true, }); return result; } } // Perform glob search const matches = globSync(pattern, { cwd: opts.cwd, absolute: opts.absolute, ignore: opts.ignore, nodir: opts.onlyFiles, }); // Filter and collect file info let files = []; for (const filePath of matches) { try { const stats = statSync(filePath); const isFile = stats.isFile(); const isDir = stats.isDirectory(); // Apply filters if (opts.onlyFiles && !isFile) continue; if (opts.onlyDirectories && !isDir) continue; if (isFile) { // Extension filter const ext = extname(filePath); if (opts.extensions.length > 0 && !opts.extensions.includes(ext)) continue; if (opts.excludeExtensions.includes(ext)) continue; // Size filter if (stats.size < opts.minSize || stats.size > opts.maxSize) continue; // Date filter if (stats.mtime < opts.modifiedAfter || stats.mtime > opts.modifiedBefore) continue; } // Build metadata if requested let metadata; if (opts.includeMetadata) { metadata = { path: filePath, relativePath: relative(opts.cwd, filePath), name: basename(filePath), extension: extname(filePath), size: stats.size, modified: stats.mtime, type: isFile ? 'file' : 'directory', fileType: isFile ? detectFileType(filePath) : undefined, }; } files.push({ path: filePath, metadata }); } catch { // Skip files we can't access continue; } } // Sort files this.sortFiles(files, opts.sortBy, opts.sortOrder); // Apply pagination const totalMatches = files.length; const paginatedFiles = files.slice(opts.offset, opts.offset + opts.limit); const truncated = totalMatches > paginatedFiles.length + opts.offset; // Build result array const results = paginatedFiles.map((f) => { if (opts.includeMetadata && f.metadata) { return f.metadata; } return f.path; }); // Add content if requested (and files are small enough) if (opts.includeContent) { for (let i = 0; i < results.length; i++) { const filePath = typeof results[i] === 'string' ? results[i] : results[i].path; try { const stats = statSync(filePath); if (stats.isFile() && stats.size <= opts.maxContentSize) { const content = readFileSync(filePath, 'utf-8'); if (typeof results[i] === 'object') { results[i].content = content; } } } catch { // Skip content for files we can't read } } } // Calculate tokens const resultTokens = this.tokenCounter.count(JSON.stringify(results)).tokens; // Estimate original tokens (if we had returned all content) let originalTokens = resultTokens; if (!opts.includeContent && !opts.includeMetadata) { // Path-only mode: estimate content would be 50x more tokens originalTokens = resultTokens * 50; } else if (!opts.includeContent) { // Metadata mode: estimate content would be 10x more tokens originalTokens = resultTokens * 10; } const tokensSaved = originalTokens - resultTokens; const compressionRatio = resultTokens / originalTokens; // Build result const result = { success: true, pattern, metadata: { totalMatches, returnedCount: results.length, truncated, tokensSaved, tokenCount: resultTokens, originalTokenCount: originalTokens, compressionRatio, duration: 0, // Will be set below cacheHit: false, }, files: results, }; // 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_glob', 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_glob', duration, inputTokens: 0, outputTokens: 0, cachedTokens: 0, savedTokens: 0, success: false, cacheHit: false, }); return { success: false, pattern, metadata: { totalMatches: 0, returnedCount: 0, truncated: false, tokensSaved: 0, tokenCount: 0, originalTokenCount: 0, compressionRatio: 0, duration, cacheHit: false, }, error: error instanceof Error ? error.message : String(error), }; } } /** * Sort files by specified field */ sortFiles(files, sortBy, sortOrder) { files.sort((a, b) => { let comparison = 0; switch (sortBy) { case 'name': comparison = basename(a.path).localeCompare(basename(b.path)); break; case 'size': if (a.metadata && b.metadata) { comparison = a.metadata.size - b.metadata.size; } break; case 'modified': if (a.metadata && b.metadata) { comparison = a.metadata.modified.getTime() - b.metadata.modified.getTime(); } break; case 'path': default: comparison = a.path.localeCompare(b.path); break; } return sortOrder === 'desc' ? -comparison : comparison; }); } /** * Get glob statistics */ getStats() { const globMetrics = this.metrics.getOperations(0, 'smart_glob'); const totalGlobs = globMetrics.length; const cacheHits = globMetrics.filter((m) => m.cacheHit).length; const totalTokensSaved = globMetrics.reduce((sum, m) => sum + (m.savedTokens || 0), 0); const totalInputTokens = globMetrics.reduce((sum, m) => sum + (m.inputTokens || 0), 0); const totalOriginalTokens = totalInputTokens + totalTokensSaved; const averageReduction = totalOriginalTokens > 0 ? (totalTokensSaved / totalOriginalTokens) * 100 : 0; return { totalGlobs, cacheHits, totalTokensSaved, averageReduction, }; } } /** * Get smart glob tool instance */ export function getSmartGlobTool(cache, tokenCounter, metrics) { return new SmartGlobTool(cache, tokenCounter, metrics); } /** * CLI function - Creates resources and uses factory */ export async function runSmartGlob(pattern, options = {}) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache'), 100); const tokenCounter = new TokenCounter(); const metrics = new MetricsCollector(); const tool = getSmartGlobTool(cache, tokenCounter, metrics); return tool.glob(pattern, options); } /** * MCP Tool Definition */ export const SMART_GLOB_TOOL_DEFINITION = { name: 'smart_glob', description: 'Search files with glob patterns and 75% token reduction through path-only results and smart filtering', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Glob pattern to match files (e.g., "src/**/*.ts", "*.json")', }, cwd: { type: 'string', description: 'Working directory for glob search', }, includeMetadata: { type: 'boolean', description: 'Include file metadata (size, modified date, etc.)', default: false, }, includeContent: { type: 'boolean', description: 'Include file content for small files', default: false, }, extensions: { type: 'array', items: { type: 'string' }, description: 'Filter by file extensions (e.g., [".ts", ".js"])', }, limit: { type: 'number', description: 'Maximum number of results to return', }, sortBy: { type: 'string', enum: ['name', 'size', 'modified', 'path'], description: 'Field to sort results by', default: 'path', }, }, required: ['pattern'], }, }; //# sourceMappingURL=smart-glob.js.map