MCPcopy Index your code
hub / github.com/dmtrKovalenko/fff

github.com/dmtrKovalenko/fff @0.9.7-nightly.3826905

Chat with this repo
repository ↗ · DeepWiki ↗ · release 0.9.7-nightly.3826905 ↗ · + Follow
2,050 symbols 7,297 edges 137 files 468 documented · 23%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

FFF

A file search toolkit for humans and AI agents. Really fast.

Typo-resistant path and content search, frecency-ranked file access, a background watcher, and a lightweight in-memory content index. Way faster than CLIs like ripgrep and fzf in any long-running process that searches more than once.

Powers file search in opencode, nushell, and many more amazing projects!

Originally started as Neovim plugin people loved, but it turned out that plenty of AI harnesses and code editors need the same thing: accurate, fast file search as a library. That is what fff is.


Pick what you are interested in:

MCP server

Works with Claude Code, Codex, OpenCode, Cursor, Cline, and any MCP-capable client. Fewer grep roundtrips, less wasted context, faster answers.

Benchmark chart comparing FFF against the built-in AI file-search tools

One-line install

Linux / macOS:

curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex

The scripts live at install-mcp.sh and install-mcp.ps1 if you want to read them first. They print the exact wiring instructions for your client.

Homebrew (macOS / Linux)

brew install dmtrKovalenko/fff/fff-mcp
brew upgrade fff-mcp   # after new stable releases

Formula lives in Formula/fff-mcp.rb in this repo and is auto-bumped on every stable release (see bump-homebrew-formula in .github/workflows/release.yaml). Installs the prebuilt fff-mcp binary from GitHub releases.

Once the server is connected, ask the agent to "use fff" and it picks up the ffgrep, fffind, and fff-multi-grep tools.

Recommended agent prompt

Drop this into your project's CLAUDE.md or equivalent:

For any file search or grep in the current git-indexed directory, use fff tools.

What changes

  • Frecency memory. Files you actually open rank higher next time. Warm-up from git touch history runs automatically.
  • Definition-first hinting. Lines that look like code definitions are classified on the Rust side, no regex overhead in your prompt.
  • Smart-case with auto-fuzzy fallback. IsOffTheRecord finds snake_case variants; zero-match queries retry as fuzzy and surface the best approximate hits.
  • Git-aware annotations. Modified, untracked, and staged files are tagged so the agent reaches for what you are actively changing.

Source: crates/fff-mcp/.

The MCP server gives any agent a file search tool that is faster and more token-efficient than the built-in one.

Pi agent extension

Install

pi install npm:@ff-labs/pi-fff

Modes

Three operating modes, switchable at runtime with /fff-mode:

Mode What it does
tools-and-ui (default) Adds ffgrep and fffind tools, replaces @-mention autocomplete with FFF.
tools-only Only tool injection. Keeps pi's native editor autocomplete.
override Replaces pi's built-in grep, find, and multi_grep with FFF implementations.

Env vars: PI_FFF_MODE, FFF_FRECENCY_DB, FFF_HISTORY_DB. Flags: --fff-mode, --fff-frecency-db, --fff-history-db.

Agent-facing tools

  • ffgrep. Content search. Accepts path, exclude (comma, space, or array; leading ! optional), caseSensitive, context, and cursor pagination. Auto-detects regex, falls back to fuzzy on zero exact matches, rejects .*-style wildcard-only patterns up front.
  • fffind. Path and filename search. Matches the whole repo-relative path, not just the filename. Frecency-aware. The weak-match detector flags scattered fuzzy noise before it floods the agent's context.

Commands

  • /fff-mode [tools-and-ui | tools-only | override]. Show or switch the mode.
  • /fff-health. Picker, frecency, and git integration status.
  • /fff-rescan. Force a rescan.

Source: packages/pi-fff/.

The Pi extension swaps pi's native tools for FFF implementations and feeds the interactive editor's @-mention autocomplete from the frecency-ranked index.

fff.nvim

Demo on the Linux kernel repo (100k files, 8GB):

https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb

Installation

lazy.nvim

{
  'dmtrKovalenko/fff.nvim',
  build = function()
    -- downloads a prebuilt binary or falls back to cargo build
    require("fff.download").download_or_build_binary()
  end,
  -- for nixos:
  -- build = "nix run .#release",
  opts = {
    debug = {
      enabled = true,
      show_scores = true,
    },
  },
  lazy = false, -- the plugin lazy-initialises itself
  keys = {
    { "ff", function() require('fff').find_files() end, desc = 'FFFind files' },
    { "fg", function() require('fff').live_grep() end, desc = 'LiFFFe grep' },
    { "fz",
      function() require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) end,
      desc = 'Live fffuzy grep',
    },
    { "fw",
      function() require('fff').live_grep_under_cursor() end,
      mode = { 'n', 'x' },
      desc = 'Search current word / selection',
    },
  },
}

vim.pack

vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })

vim.api.nvim_create_autocmd('PackChanged', {
  callback = function(ev)
    local name, kind = ev.data.spec.name, ev.data.kind
    if name == 'fff.nvim' and (kind == 'install' or kind == 'update') then
      if not ev.data.active then vim.cmd.packadd('fff.nvim') end
      require('fff.download').download_or_build_binary()
    end
  end,
})

vim.g.fff = {
  lazy_sync = true,
  debug = { enabled = true, show_scores = true },
}

vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FFFind files' })

Public API

require('fff').find_files()                        -- find files in current repo
require('fff').live_grep()                         -- live content grep
require('fff').live_grep_under_cursor()            -- grep <cword> in normal, selection in visual
require('fff').scan_files()                        -- force rescan
require('fff').refresh_git_status()                -- refresh git status
require('fff').find_files_in_dir(path)             -- find in a specific dir
require('fff').change_indexing_directory(new_path) -- change root

-- Programmatic search (no UI). Useful for plugin integrations.
require('fff').file_search(query, opts)            -- fuzzy search files / dirs / mixed
require('fff').content_search(query, opts)         -- programmatic grep

file_search(query, opts)

Returns a structured result { items, scores, total_matched, total_files?, total_dirs?, location? }. Each item has a type field ("file" or "directory") and name / relative_path. File items also expose size, modified, git_status, is_binary, and frecency scores.

local r = require('fff').file_search('button', {
  mode             = 'mixed',  -- 'files' (default) | 'directories' | 'mixed'
  max_results      = 50,
  page             = 0,        -- 0-based pagination
  current_file     = nil,      -- path to deprioritize for distance scoring
  max_threads      = 4,
  cwd              = nil,      -- switch indexed root if different (see below)
  wait_for_index_ms = nil,     -- override the default scan wait timeout
})
for _, item in ipairs(r.items) do
  print(item.type, item.relative_path)
end

content_search(query, opts)

Returns a GrepResult { items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }. Each match item has relative_path, name, line_number, col, line_content, match_ranges, plus the same file metadata as file_search.

local r = require('fff').content_search('TODO', {
  mode                  = 'plain',  -- 'plain' (default) | 'regex' | 'fuzzy'
  max_file_size         = 10 * 1024 * 1024,
  max_matches_per_file  = 100,
  smart_case            = true,
  page_size             = 50,
  file_offset           = 0,
  time_budget_ms        = 0,
  trim_whitespace       = false,
  cwd                   = nil,      -- switch indexed root if different
  wait_for_index_ms     = nil,      -- override the default scan wait timeout
})
for _, m in ipairs(r.items) do
  print(string.format('%s:%d %s', m.relative_path, m.line_number, m.line_content))
end

Both functions accept the same constraint syntax as the UI pickers (e.g. git:modified, *.rs, !test/, glob patterns).

cwd and indexing

Both file_search and content_search honour an optional cwd field. The first call to either function lazily initialises the picker at config.base_path (your Neovim cwd by default).

  • If cwd matches the currently indexed root, the call returns immediately against the existing index.
  • If cwd differs, the picker is re-indexed at the new root and the call blocks (default up to 10 s) until the new picker is installed and its initial scan completes — so callers always get results from the right tree.
  • If the index is still warming up after a change_indexing_directory, you can pass wait_for_index_ms = N to block for up to N ms regardless of whether cwd triggered the swap. Pass 0 to skip waiting entirely (useful for fire-and-forget calls where partial results are acceptable).
  • Invalid or non-existent cwd paths return an empty result and emit an error via vim.notify.

Commands

  • :FFFScan. Rescan files.
  • :FFFRefreshGit. Refresh git status.
  • :FFFClearCache [all|frecency|files]. Clear caches.
  • :FFFHealth. Health check.
  • :FFFDebug [on|off|toggle]. Toggle the scoring display.
  • :FFFOpenLog. Open ~/.local/state/nvim/log/fff.log.

Configuration

Defaults are sensible. Override only what you care about.

``lua require('fff').setup({ base_path = vim.fn.getcwd(), prompt = '> ', title = 'FFFiles', max_results = 100, max_threads = 4, lazy_sync = true, prompt_vim_mode = false, follow_symlinks = false, -- Allow indexing the user's $HOME directory. Enabled by default. -- Disable if you strictly sure you don't want this, as it makes whole fff error hard enable_home_dir_scanning = true, -- Allow indexing a filesystem root (e.g./,C:`). Disabled by default enable_fs_root_scanning = false, layout = { height = 0.8, width = 0.8, prompt_position = 'bottom', -- or 'top' preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom' preview_size = 0.5, -- Border style for the picker windows. Leave unset (nil) to follow the -- global vim.o.winborder; set it to override fff's borders independently. border = nil, -- 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'none' flex = { size = 130, wrap = 'top' }, min_list_height = 10, -- do not display anything except the list below this threshold show_scrollbar = true, path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' | 'start' anchor = 'center', }, preview = { enabled = true, max_size = 10 * 1024 * 1024, chunk_size = 8192, binary_file_threshold = 1024, imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', line_numbers = false, cursorlineopt = 'both', wrap_lines = false, filetypes = { svg = { wrap_lines = true }, markdown = { wrap_lines = true }, text = { wrap_lines = true }, }, }, keymaps = { close = '', select = '', select_split = '', select_vsplit = '', select_tab = '', move_up = { '', '' }, move_down = { '', '' }, preview_scroll_up = '', preview_scroll_down = '', toggle_debug = '', cycle_grep_modes = '', -- grep mode only: jump cursor to first match of next/prev file group grep_jump_to_next_file = { '', '' }, grep_jump_to_prev_file = { '', '' }, cycle_previous_query = '', toggle_select = '', send_to_quickfix = '', focus_list = 'l', focus_preview = 'p', }, frecency = { enabled = true, db_path = vim.fn.stdpath('cache') .. '/fff_nvim', }, history = { enabled = true, db_path = vim.fn.stdpath('data') .. '/fff_queries', min_combo_count = 3, combo_boost_score_multiplier = 100, }, git = { status_text_color = false, -- true to color filenames by git status }, select = { -- Return winid to open the chosen file in, or nil to open in the original window select_window = function(current_buf, action) --[[ default impl ]] end, }, grep = { max_file_size = 10 * 1024 * 1024, max_matches_per_file = 100, smart_case = true, time_budget_ms = 150, modes = { 'plain', 'regex', 'fuzzy' }, trim_whitespace = false, enable_filename_constraint = false, -- treat filename-like tokens (e.g. score.rs) in a grep query as a file-path filter scoping the search; off = searched as literal text location_format = ':%d:%d', -- printf format for line:col prefix in grep results, e.g. ':%d' for line-only }, debug = { enabled = false, -- show the file info panel next to

Extension points exported contracts — how you extend this code

ParserConfig (Interface)
Parser configuration trait - allows different picker types to customize parsing [7 implementers]
crates/fff-query-parser/src/config.rs
Sink (Interface)
A trait that defines how results from searchers are handled. The searcher follows the "push" model: the searcher drives [4 …
crates/fff-grep/src/sink.rs
FFFStringStorage (Interface)
Different sources of the string storage used by FFF implements as a deduplicated 16-bytes alined heap can be stored in R [2 …
crates/fff-core/src/types.rs
FileFinderApi (Interface)
(no doc) [3 implementers]
packages/fff-node/src/fff-api.ts
FileFinderApi (Interface)
(no doc) [3 implementers]
packages/fff-bun/src/fff-api.ts
IntoLuaResult (Interface)
Extension trait for Result to convert to LuaResult [1 implementers]
crates/fff-nvim/src/error.rs
FileFinderApi (Interface)
(no doc) [2 implementers]
packages/shared/fff-api.ts
ToolNames (Interface)
(no doc)
packages/pi-fff/src/index.ts

Core symbols most depended-on inside this repo

len
called by 363
crates/fff-core/src/stable_vec.rs
push
called by 206
crates/fff-core/src/stable_vec.rs
clone
called by 200
crates/fff-core/src/types.rs
parse
called by 200
crates/fff-query-parser/src/parser.rs
read
called by 197
crates/fff-core/src/shared.rs
is_empty
called by 186
crates/fff-grep/src/matcher.rs
len
called by 164
crates/fff-grep/src/matcher.rs
collect
called by 161
crates/fff-core/src/constraints.rs

Shape

Function 1,173
Method 600
Class 173
Interface 84
Enum 20

Languages

Rust78%
TypeScript19%
Python2%
C++1%
C1%
Ruby1%

Modules by API surface

crates/fff-core/src/file_picker.rs93 symbols
crates/fff-query-parser/src/parser.rs84 symbols
crates/fff-core/src/types.rs70 symbols
crates/fff-core/tests/grep_integration.rs69 symbols
crates/fff-core/src/bigram_filter.rs65 symbols
packages/fff-node/src/ffi.ts58 symbols
crates/fff-c/src/accessors.rs55 symbols
crates/fff-core/src/grep.rs52 symbols
crates/fff-core/src/constraints.rs52 symbols
crates/fff-core/tests/fuzz_git_watcher_stress.rs46 symbols
packages/fff-bun/src/ffi.ts45 symbols
crates/fff-c/src/lib.rs45 symbols

For agents

$ claude mcp add fff \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page