Download apps/shared/src/fuzzy.ts from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 5.67 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/shared/src/fuzzy.ts
- Command line
-
hf download hf://SaylorTwift/hermes-agent/apps/shared/src/fuzzy.ts
-
curl -L -o fuzzy.ts https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/shared/src/fuzzy.ts
5.67 kB
| // Lightweight fuzzy subsequence scorer for picker filtering. | |
| // | |
| // Matches a query as an ordered subsequence of the target (so `g4o` matches | |
| // `gpt-4o`) and scores by match quality so callers can rank results. Higher | |
| // score is a better match. Returns the matched character indices so callers | |
| // can highlight them. | |
| // | |
| // The scoring favours, in rough order: exact full match, prefix match, matches | |
| // that start on a word boundary (after `-`, `_`, `/`, `.`, space, or a | |
| // lower→upper case transition), contiguous runs, and earlier matches. This is | |
| // intentionally simple — no external dependency — but good enough to make | |
| // `son4` rank `claude-sonnet-4` above an incidental scattered hit. | |
| // | |
| // Separators (`-`, `_`, `.`) fold to a space on BOTH sides before matching, so | |
| // `gpt.4o`, `claude_3` and `qwen3-8` hit `gpt-4o`, `claude-3-opus` and | |
| // `qwen3.8-flash` the way a user typing from memory expects. The fold is one | |
| // char in, one char out, so returned positions still index the original | |
| // target and a highlighter can apply the same fold without drift. | |
| // | |
| // Shared by the desktop, web and TUI pickers via `@hermes/shared/fuzzy` so a | |
| // query ranks identically on every surface. | |
| export interface FuzzyMatch { | |
| /** Total score; higher is better. */ | |
| score: number | |
| /** Indices into the original (non-lowercased) target that were matched. */ | |
| positions: number[] | |
| } | |
| const WORD_BOUNDARY = /[-_/.\s]/ | |
| /** Length-preserving search fold: lower-case and `[-_.]` → space. */ | |
| export function searchFold(value: string): string { | |
| return value.toLowerCase().replace(/[-_.]/g, ' ') | |
| } | |
| function isBoundary(target: string, index: number): boolean { | |
| if (index === 0) { | |
| return true | |
| } | |
| const prev = target[index - 1] | |
| if (WORD_BOUNDARY.test(prev)) { | |
| return true | |
| } | |
| // camelCase / lower→upper transition (e.g. the `O` in `gptO`). | |
| const cur = target[index] | |
| return prev === prev.toLowerCase() && cur !== cur.toLowerCase() && cur === cur.toUpperCase() | |
| } | |
| /** | |
| * Score a single query token against a target. Returns null when the token is | |
| * not a subsequence of the target. An empty query scores 0 with no positions. | |
| */ | |
| export function fuzzyScore(target: string, query: string): FuzzyMatch | null { | |
| if (!query) { | |
| return { score: 0, positions: [] } | |
| } | |
| const lowerTarget = searchFold(target) | |
| const lowerQuery = searchFold(query) | |
| const positions: number[] = [] | |
| let score = 0 | |
| let prevIndex = -1 | |
| let searchFrom = 0 | |
| for (const ch of lowerQuery) { | |
| const idx = lowerTarget.indexOf(ch, searchFrom) | |
| if (idx < 0) { | |
| return null | |
| } | |
| positions.push(idx) | |
| // Base point for the matched character. | |
| score += 1 | |
| // Contiguous with the previous match → strong bonus. | |
| if (prevIndex >= 0 && idx === prevIndex + 1) { | |
| score += 5 | |
| } else if (prevIndex >= 0) { | |
| // Penalise the gap we had to skip (capped), so contiguous beats scattered. | |
| score -= Math.min(idx - prevIndex - 1, 3) | |
| } | |
| // Word-boundary / start-of-string matches are meaningful. | |
| if (isBoundary(target, idx)) { | |
| score += 3 | |
| } | |
| // Matching the very first character of the target is the strongest signal. | |
| if (idx === 0) { | |
| score += 5 | |
| } | |
| prevIndex = idx | |
| searchFrom = idx + 1 | |
| } | |
| // Prefix bonus: the query matched a contiguous prefix of the target. | |
| if (positions.length && positions[0] === 0 && positions[positions.length - 1] === positions.length - 1) { | |
| score += 8 | |
| } | |
| // Exact full match dominates everything else. | |
| if (lowerTarget === lowerQuery) { | |
| score += 20 | |
| } | |
| // Slightly prefer shorter targets when scores are otherwise close, so a | |
| // query that fully prefixes a short id beats the same prefix on a long one. | |
| score -= lowerTarget.length * 0.01 | |
| return { score, positions } | |
| } | |
| /** | |
| * Score a target against a whitespace-separated, multi-token query. Every token | |
| * must match (AND semantics); the result aggregates per-token scores and the | |
| * union of matched positions. Returns null if any token fails to match. | |
| */ | |
| export function fuzzyScoreMulti(target: string, query: string): FuzzyMatch | null { | |
| const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean) | |
| if (!tokens.length) { | |
| return { score: 0, positions: [] } | |
| } | |
| let score = 0 | |
| const positionSet = new Set<number>() | |
| for (const token of tokens) { | |
| const match = fuzzyScore(target, token) | |
| if (!match) { | |
| return null | |
| } | |
| score += match.score | |
| for (const pos of match.positions) { | |
| positionSet.add(pos) | |
| } | |
| } | |
| return { score, positions: [...positionSet].sort((a, b) => a - b) } | |
| } | |
| export interface RankedItem<T> { | |
| item: T | |
| score: number | |
| positions: number[] | |
| } | |
| /** | |
| * Filter + rank a list by a fuzzy query against a derived text key. Non-matching | |
| * items are dropped; matches are sorted by score (descending), ties broken by | |
| * the original index so ordering is stable for equal scores. An empty query | |
| * returns every item in original order with no positions. | |
| */ | |
| export function fuzzyRank<T>(items: readonly T[], query: string, toText: (item: T) => string): RankedItem<T>[] { | |
| const trimmed = query.trim() | |
| if (!trimmed) { | |
| return items.map(item => ({ item, score: 0, positions: [] })) | |
| } | |
| const ranked: Array<RankedItem<T> & { index: number }> = [] | |
| items.forEach((item, index) => { | |
| const match = fuzzyScoreMulti(toText(item), trimmed) | |
| if (match) { | |
| ranked.push({ item, score: match.score, positions: match.positions, index }) | |
| } | |
| }) | |
| ranked.sort((a, b) => b.score - a.score || a.index - b.index) | |
| return ranked.map(({ item, score, positions }) => ({ item, score, positions })) | |
| } | |