find_files, grep, and multi_grep only read the filesystem but declared
no MCP tool annotations, so plan-mode / read-only clients blocked them.
Add annotations(read_only_hint = true, destructive_hint = false,
open_world_hint = false) to each #[tool].
Closes#771
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
ffgrep/fff-multi-grep passed the documented `limit` only as
`maxMatchesPerFile`, so matches spread across files could return a full
SDK page (default 50) instead of `limit`. Pass `limit` as `pageSize`
too, on both grep tools and the fuzzy fallback, so the cap applies to
total matches per page; excess stays retrievable via the existing
cursor. Also clamp `context` to a bounded non-negative integer so a
large value cannot multiply output size past the model window.
Closes#768
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* chore: point residual repo refs at dmtrKovalenko/fff
Update install URLs, download targets, and docs after the GitHub
rename from fff.nvim to fff. Preserve the existing Neovim package
name (fff.nvim) in lazy/vim.pack snippets so upgraders keep the same
install dir and lockfile identity.
* chore(docs): align Neovim package name with repo basename
Drop the explicit fff.nvim package name so lazy/vim.pack use fff from
the repository URL. Mild migration cost for existing installs (new
plugin dir + clean of the old one). Drop this commit to keep the
lowest-pain name=fff.nvim install snippets from the previous commit.
* fix(grep): keep FilePath scope in regex/literal fallback (#756)
The literal/regex fallback rebuilt the query with empty constraints,
dropping an explicit inline FilePath scope. In regex mode a top-level
alternation then leaked matches into files outside the pinned path.
Preserve FilePath constraints in the fallback query.
Closes#756
* chore: cargo fmt (#756)
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
The no_content_indexing field lacked its #[arg(long = ...)] attribute,
so clap treated it as a positional with a SetTrue action. Debug builds
panicked on any invocation; release builds exposed [NO_CONTENT_INDEXING]
as a positional and rejected --no-content-indexing.
Closes#754
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Pi reloads extension modules with jiti moduleCache:false, so loadSdk()
re-executes a dynamic import of the fff-bun module graph on every /reload.
The fff-bun graph top-level awaits a type:file import of the native .so,
which never resolves when re-imported inside the Bun-compiled pi binary,
leaving the reload screen stuck forever (pi awaits session_start handlers
without a timeout).
Cache the first import on globalThis so reloads reuse it.
Closes#757
Co-authored-by: chenydev <chenydev@users.noreply.github.com>
* fix(nvim): refuse fs-root/home index at Lua level before FFI (#745)
Opening nvim at `/` with lazy=false crashed the whole neovim process on
CI-cross-compiled aarch64 .so binaries: the init_file_picker FFI call
SIGSEGVs instead of returning Error::FilesystemRoot cleanly. A SIGSEGV
is a hardware signal that the pcall around init_file_picker cannot catch.
Mirror the Rust refusal (file_picker.rs:862) in Lua and bail before
crossing the FFI boundary, so the crashing path is never reached. Same
guard added to change_indexing_directory for the :cd-into-root case.
Honors enable_fs_root_scanning / enable_home_dir_scanning overrides.
* chore(nvim): trim comments in fs-root/home refusal guard
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Expose home-dir scanning as --fff-enable-home-scan flag + FFF_ENABLE_HOME_SCAN
env, default true. resolveBoolOpt takes a fallback and accepts 0/false. Threaded
through the main finder and AuxFinderPool.
Cache os.homedir() once per process in src/paths.ts. AuxOpts.onHomeDirScan fires
whenever the agent spawns an aux picker rooted at or above $HOME. On session
start from $HOME, notify the user and track scan progress with a 1s poller that
clears the footer once the scan settles; the interval is unref'd and cleared on
shutdown and in destroyFinder().
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dmtrKovalenko <16926049+dmtrKovalenko@users.noreply.github.com>
* fix: Correctly handle empty directories during the scan
Closes#725
Before we have completely ignored empty directories partially as a
feature cause usually they do not contain anything useful but there is a
bug #725 that we need to fix and it definetely makes sense to show empty
directories in the dir search
* fix: Gitignore incompatbility
Closes https://github.com/dmtrKovalenko/fff/issues/723 fixed in zlob
* more efficient way to track subdirs
* fix(pi-fff): dedup concurrent aux finders and bound grep time (#746)
Concurrent AuxFinderPool.acquire() calls for the same root each started a
full scan because entries was populated only after waitForScan() resolved.
Coalesce in-flight creations by root via a pending map. Also pass a finite
timeBudgetMs to native grep (sync call, uninterruptible by AbortSignal) and
skip the fuzzy fallback when the exact pass left a nextCursor.
Refs #746
* Apply suggestions from code review
Co-authored-by: Dmitriy Kovalenko <dmitriy@iusevimbtw.com>
* fix typo
Co-authored-by: Dmitriy Kovalenko <dmitriy@iusevimbtw.com>
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmitriy@iusevimbtw.com>
Per pi extension docs, each guideline in promptGuidelines is appended
to the flat Guidelines block without tool grouping, so the LLM cannot
tell which tool owns which bullet. Prefix each entry with the dynamic
tool name (toolNames.grep / toolNames.find / toolNames.multiGrep) so
guidelines stay correct in override mode too.
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
git2 0.20.4 carries two informational=unsound advisories that surface in
downstream cargo-audit/cargo-deny runs. fff does not call the affected
APIs (Remote::list, Blame::blame_buffer), but bumping clears the noise
for consumers.
0.21.0 changes StatusEntry::path() to return Result<&str, git2::Error>
instead of Option<&str>; adjust the two callers in fff-core.
Closes#733
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* fix(fff-mcp): compare CARGO_PKG_VERSION to stable release tag (#721)
The update check compared the embedded 40-char git SHA prefix against
the release tag string, which never matched a semver tag. It also hit
/releases?per_page=1 (prerelease-inclusive) while install-mcp.sh is
pinned to a stable tag, so the suggested installer could not silence
the notice when the API returned a nightly.
Compare CARGO_PKG_VERSION against /releases/latest (stable-only) with
a leading `v` stripped from the tag.
Closes#721
* test(fff-node): retry watch subscribe in exit test
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
fff-core gates indexing of $HOME and / behind enable_home_dir_scanning and
enable_fs_root_scanning, and every other surface (C, python, nvim, node/bun)
exposes them. fff-mcp did not, so it aborts at startup whenever an editor or
agent launches it from a home directory.
Add --enable-home-scan and --enable-root-scan, also settable via
FFF_ENABLE_HOME_SCAN and FFF_ENABLE_ROOT_SCAN. Both default off, so the guard
is unchanged unless opted into.
Allow users to pass a custom border table instead of just preset
strings. Supports a table { border_chars, junction_chars } and falls
back to existing preset system when a string is passed
* fix(pi-fff): preserve path and exclude constraints in fuzzy grep fallback (#697)
The automatic fuzzy fallback in the pi-fff `grep` tool passed the raw
`pattern` to `picker.grep`, discarding the constrained `query` built via
`buildQuery`. As a result, the fallback ignored the caller's `path` and
`exclude` constraints and could return matches from explicitly excluded
directories or files outside the requested path.
Pass the constrained `query` to the fallback instead, so it only broadens
matching (fuzzy vs. plain) without broadening scope.
Closes#697
* fix(pi-fff): drop path constraint in fuzzy fallback only for file paths
When the caller pinned a specific file (path has an extension), the
fuzzy fallback broadens across the whole picker so a mistyped filename
can still surface matches. For directory constraints (or no path), keep
the constrained query so the fallback does not leak matches from
excluded / out-of-scope directories.
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
LMDB envs may only be opened once per process. AuxFinderPool was forwarding
the main finder's frecencyDbPath / historyDbPath into every aux FileFinder,
so the first out-of-workspace search failed with
"environment already open in this program".
Aux finders are transient and per-search; they run without persistent
frecency/history scoring.
Closes#700
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Adds @ff-labs/fff-bin-android-arm64 platform package and wires the
existing aarch64-linux-android CI build into the npm publishing matrix.
Extends getTriple() in fff-node and fff-bun to map process.platform
'android' to linux-android, and adds android to os arrays with the new
optionalDependency. Closes#692.
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
On Windows, `path.relative()` returns an absolute path when the source
and target are on different drives (e.g. `D:\` → `C:\`). The existing
check in `routePathConstraint()` only recognizes `".."` and `"..\..."`
as indicators that a path is outside the workspace — it misses the
cross-volume case entirely. The path is then treated as workspace-local,
and downstream code rejects it with:
Path constraint must be relative to the workspace
Extract the workspace-outside check into
`isOutsideWorkspaceRelativePath()` so the logic is testable in
isolation, and add `path.isAbsolute()` to the condition. A cross-volume
relative result is by definition outside the workspace.
Add a Windows-specific regression test that verifies the helper
recognizes a cross-volume `path.win32.relative()` result as outside the
workspace. The test is gated on `process.platform === "win32"` and has
no effect on Linux or macOS CI runs.
PR #669 made sdk.ts dynamically import @ff-labs/fff-bun when running under
bun, but only added it as an optional peer dependency. Package managers do
not install optional peers by default, so `pi install npm:@ff-labs/pi-fff`
on a bun-only host produced a node_modules tree without @ff-labs/fff-bun
and pi crashed at session_start with "Cannot find module '@ff-labs/fff-bun'".
Promote both SDKs to regular deps. Bundle size cost is minor (SDKs are TS
shims over the same optional native bins) and this guarantees the dynamic
import in sdk.ts always resolves regardless of runtime or installer.
Closes#689
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* fix(pi-fff): route out-of-workspace path constraints to a rotating aux finder pool
Hotfix prototype for #463. When the agent passes an absolute `path`
outside the workspace cwd to ffgrep/fffind, spin up (or reuse) a
FileFinder rooted at that path instead of throwing
"Path constraint must be relative to the workspace".
Pool keeps at most 3 aux finders, LRU-evicted, dropped after 5 minutes
of inactivity. Find pagination cursors carry the aux root so resumes
hit the same finder.
* fix: Redesign the aux finder
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* fix(picker_ui): highlight fuzzy query matches on file picker
* fix(file-picker): gate fuzzy query highlights behind config
* fix(file-picker): use rust fuzzy match ranges for highlights
Compute fuzzy match byte ranges on the Rust side for paginated file results and pass them through the nvim Lua binding.
* fix(pi-fff): isomorphic runtime detection with lazy SDK load
Detect bun vs node at runtime and dynamically import the matching SDK
(@ff-labs/fff-bun on bun, @ff-labs/fff-node on node) via a variable
package name so oh-my-pi's static extension validator does not chase
ffi-rs' optional native binaries through the module graph.
Closes#668
* fix(pi-fff): fail loud on wrong-runtime SDK
Drop the cross-runtime SDK fallback in loadSdk. Falling back to the
node SDK on a bun host re-introduces the ffi-rs cost this PR is meant
to avoid, and hides packaging bugs where the correct SDK is missing.
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>