/** * Smart Grep Tool - 80% Token Reduction * * Achieves token reduction through: * 1. Match-only output (line numbers + matched text, not full files) * 2. Context line control (configurable before/after lines) * 3. Pattern caching (reuse search results) * 4. Result pagination (limit matches returned) * 5. Smart file filtering (skip binary, node_modules, etc.) * * Target: 80% reduction vs returning full file contents */ import { type TruncationReason } from '../shared/bounded-traversal.js'; import { CacheEngine } from '../../core/cache-engine.js'; import { TokenCounter } from '../../core/token-counter.js'; import { MetricsCollector } from '../../core/metrics.js'; export interface GrepMatch { file: string; lineNumber: number; column?: number; line: string; match: string; before?: string[]; after?: string[]; } export interface SmartGrepOptions { /** * Directory to search. The natural name for it, and the one the hook's * refusal message leads callers to pass; takes precedence over `cwd`. */ path?: string; cwd?: string; files?: string[]; caseSensitive?: boolean; wholeWord?: boolean; regex?: boolean; extensions?: string[]; excludeExtensions?: string[]; skipBinary?: boolean; ignore?: string[]; includeContext?: boolean; contextBefore?: number; contextAfter?: number; includeColumn?: boolean; maxMatchesPerFile?: number; limit?: number; offset?: number; filesWithMatches?: boolean; count?: boolean; /** * Serve a previously cached result for the same query. * * DEFAULT FALSE, and the default is the point. A cached search is keyed on * the query, and a query does not describe the tree it ran against: create, * edit or delete a matching file and the cached answer is simply wrong. * Measured live -- a file created between two identical searches did not * appear in the second. * * Enabling it says "nothing outside this server is changing these files", * which only the caller can know. Writes made THROUGH this server are * handled either way: they bump a generation counter that forms part of the * key, so our own edits always invalidate. */ useCache?: boolean; ttl?: number; maxFileSize?: number; /** * Wall-clock budget for discovery AND reading, in ms. * * Defaults to TOKEN_OPTIMIZER_TRAVERSAL_DEADLINE_MS, then to 10 s. The * search returns what it found and says it was cut short, rather than * running until the caller's own tool timeout kills it. */ deadlineMs?: number; encoding?: BufferEncoding; } export interface SmartGrepResult { success: boolean; pattern: string; metadata: { totalMatches: number; filesSearched: number; /** * True when a BOUND stopped the search, so the tree was not fully * covered. Distinct from `truncated`, which only means more matches * exist than this page returned. */ searchTruncated?: boolean; /** Which bound stopped it: a result cap, or the wall-clock deadline. */ searchTruncatedBy?: TruncationReason; /** What to do about it, in the caller's terms. */ searchNote?: string; filesWithMatches: number; returnedMatches: number; truncated: boolean; tokensSaved: number; tokenCount: number; originalTokenCount: number; compressionRatio: number; duration: number; cacheHit: boolean; savingsClassification?: 'unmeasured'; savingsReason?: string; }; matches?: GrepMatch[]; files?: string[]; /** * Match counts per file, when `count` is set. * * A Record rather than a Map on purpose: this crosses a JSON boundary, and a * Map serialises to `{}`. The type used to say Map, which was accurate about * the value and wrong about what the caller received. */ counts?: Record; /** * Set only when a literal search found nothing that a regex search would * have found. * * `pattern` is matched literally unless `regex: true`, so an alternation or a * character class silently means nothing and the caller gets * `{ success: true, totalMatches: 0, filesSearched: 7 }` -- indistinguishable * from a thorough search of a tree that does not contain the term. That zero * reads as evidence of absence and gets acted on as such. * * Deliberately narrow. It requires all three of: literal mode, zero matches, * and a pattern that WOULD have matched as a regex. A hint on every empty * result would be noise, and a caller who learns to ignore it is no better * off than one who never saw it. */ hint?: string; error?: string; } export declare class SmartGrepTool { private cache; private tokenCounter; private metrics; constructor(cache: CacheEngine, tokenCounter: TokenCounter, metrics: MetricsCollector); /** * Smart grep with match-only output and context control */ grep(pattern: string, options?: SmartGrepOptions): Promise; /** * Build search pattern from string */ private buildPattern; /** * The regex the caller may have meant, or null when there is nothing to warn * about. * * Null in three cases, each of which would make a hint wrong rather than * merely unhelpful: the search is already a regex search; escaping changed * nothing, so literal and regex mean the same thing; or the pattern is not * valid regex syntax, so `regex: true` would not have helped either. * * NOT global. The caller tests whole file contents with it, and a `g` regex * carries `lastIndex` between calls -- reusing one across files would skip * matches and make the hint depend on file order. */ private buildRegexProbe; /** * Check if a file is binary */ private isBinaryFile; /** * Get grep statistics */ getStats(): { totalSearches: number; cacheHits: number; totalTokensSaved: number; averageReduction: number; }; } /** * Get smart grep tool instance */ export declare function getSmartGrepTool(cache: CacheEngine, tokenCounter: TokenCounter, metrics: MetricsCollector): SmartGrepTool; /** * CLI function - Creates resources and uses factory */ export declare function runSmartGrep(pattern: string, options?: SmartGrepOptions): Promise; /** * MCP Tool Definition */ export declare const SMART_GREP_TOOL_DEFINITION: { name: string; description: string; annotations: { title: string; readOnlyHint: boolean; destructiveHint: boolean; idempotentHint: boolean; openWorldHint: boolean; }; inputSchema: { type: string; properties: { pattern: { type: string; description: string; }; path: { type: string; description: string; }; cwd: { type: string; description: string; }; files: { type: string; items: { type: string; }; description: string; }; caseSensitive: { type: string; description: string; default: boolean; }; regex: { type: string; description: string; default: boolean; }; extensions: { type: string; items: { type: string; }; description: string; }; includeContext: { type: string; description: string; default: boolean; }; contextBefore: { type: string; description: string; default: number; }; contextAfter: { type: string; description: string; default: number; }; limit: { type: string; description: string; }; filesWithMatches: { type: string; description: string; default: boolean; }; count: { type: string; description: string; default: boolean; }; wholeWord: { type: string; description: string; default: boolean; }; excludeExtensions: { type: string; items: { type: string; }; description: string; default: string[]; }; skipBinary: { type: string; description: string; default: boolean; }; deadlineMs: { type: string; description: string; }; ignore: { type: string; items: { type: string; }; description: string; default: string[]; }; includeColumn: { type: string; description: string; default: boolean; }; maxMatchesPerFile: { type: string; description: string; }; offset: { type: string; description: string; default: number; }; useCache: { type: string; description: string; default: boolean; }; ttl: { type: string; description: string; default: number; }; maxFileSize: { type: string; description: string; default: number; }; encoding: { type: string; description: string; default: string; }; }; required: string[]; }; }; //# sourceMappingURL=smart-grep.d.ts.map