Compare commits

...

59 Commits

Author SHA1 Message Date
Gustav the Bot 28fce4145a chore(fff-core): clippy 1.98 (#808)
e2e Tests / e2e (macos-latest) (push) Waiting to run
e2e Tests / e2e (ubuntu-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
e2e Tests / e2e (alpine-musl) (push) Waiting to run
Lua CI / lua-language-server type check (push) Waiting to run
Lua CI / luacheck lint (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
Python CI / Python bindings (ubuntu-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-linux-android (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-linux-android (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Build Python sdist (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (ubuntu-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Rust CI / Fuzz Tests (ubuntu-latest) (push) Waiting to run
Rust CI / Build i686-unknown-linux-gnu (push) Waiting to run
Rust CI / cargo fmt (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Spelling / Spell Check with Typos (push) Waiting to run
clippy 1.98 added chunks_exact_to_as_chunks, which fails CI under
-D warnings. SEEN_WORDS is a multiple of 8, so the remainder is
always empty and the fixed-size chunk keeps codegen identical or
better.

Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
2026-08-22 06:52:10 -07:00
Gustav the Bot d5b4abd2ac fix(fff-mcp): answer pre-initialize probes instead of exiting (#797) (#798)
e2e Tests / e2e (macos-latest) (push) Has been cancelled
e2e Tests / e2e (ubuntu-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
e2e Tests / e2e (alpine-musl) (push) Has been cancelled
Lua CI / lua-language-server type check (push) Has been cancelled
Lua CI / luacheck lint (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (ubuntu-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-linux-android (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-linux-android (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Build Python sdist (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Test (ubuntu-latest) (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (ubuntu-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
Rust CI / Build i686-unknown-linux-gnu (push) Has been cancelled
Rust CI / cargo fmt (push) Has been cancelled
Rust CI / cargo clippy (push) Has been cancelled
Spelling / Spell Check with Typos (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
rmcp 1.7.0 aborts startup on any pre-initialize request except ping, so a
client probing with server/discover (SEP-1442 stateless spec) killed the
process before it could fall back to the legacy handshake.

Wrap the stdio transport so unsupported pre-init requests get -32601 and the
server keeps waiting for initialize. Full stateless support is out of scope.

Closes #797

Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
2026-08-18 20:22:38 -07:00
Gustav the Bot e6df25388a chore(packages): replace biome with oxlint and oxfmt (#796) 2026-08-17 16:30:39 -07:00
XWIlluDelu 611dd873a3 fix(pi-fff): resolve startup config before tool registration (#793)
* fix(pi-fff): resolve startup config before tool registration

* refactor(pi-fff): resolve queued tool names lazily
2026-08-17 11:11:23 -07:00
XWIlluDelu d7c0ddc374 feat(pi-fff): add config JSON Schema (#791) 2026-08-16 20:53:34 -07:00
XWIlluDelu be2dd8d568 feat(pi-fff): support global config file (#790) 2026-08-16 10:39:50 -07:00
dmtrKovalenko f4def12a1c chore: bump fff-mcp release artifacts to v0.10.5 2026-08-16 16:34:13 +00:00
Dmitriy Kovalenko 459ebcdbdb chore: release 0.10.5
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-linux-android (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-linux-android (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Build Python sdist (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
2026-08-16 09:00:23 -07:00
Dmitriy Kovalenko f565d37175 fix(pi): New database files are not created (#780)
This make sure that we either use existing users's neovim databases or
actually create a new database
2026-08-16 08:57:22 -07:00
Dmitriy Kovalenko 0d8c257a5d fix(ci): regenaraate bun.lock (#781)
unf*ck the npm publishing CI script
2026-08-16 08:15:58 -07:00
Gustav the Bot d01cc483ca fix: raise LMDB max_readers to avoid MDB_READERS_FULL (#783) (#785)
* fix: raise LMDB max_readers to avoid MDB_READERS_FULL (#783)

heed's default reader table is 126 slots and fff opened envs in default
TLS mode, so each long-lived reader thread pinned a slot for its lifetime.
Long-lived embedders (Neovim, node agents) sharing one lock file across
many processes/threads exhausted the table with MDB_READERS_FULL.

Raise max_readers to 1024 (slots are ~64B, cost negligible) and expose
FFF_LMDB_MAX_READERS for hosts to tune. NOTLS left for maintainer.

Closes #783

* fix: open LMDB envs with MDB_NOTLS so reader slots free on txn drop (#783)

Reader slots are now tied to txn objects instead of pinned per thread
for the thread's lifetime, so long-lived embedders no longer accumulate
slots. Env/RoTxn become WithoutTls-typed; RwTxn is unaffected.

* fix(build): link advapi32 on Windows for lmdb-master-sys

mdb_env_setup_locks references InitializeSecurityDescriptor /
SetSecurityDescriptorDacl but lmdb-master-sys's build script never
links advapi32; minimal test binaries fail with LNK2019 without it.

* fix(test): link advapi32 in lmdb repro test binary on Windows

The test links heed directly and rustc elides the unused fff lib, so
build-script link flags never reach this binary; declare the dependency
on advapi32 (mdb_env_setup_locks security-descriptor APIs) in the test.

---------

Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
2026-08-16 07:26:22 -07:00
Dmitriy Kovalenko cf2130090e chore(ci): cut CI wall clock from ~50m to ~14m (#784)
* fix(ci): build the workspace once per e2e job

* perf(ci): cache rust compilations with sccache

* fix(ci): stop sccache idling out mid-compile and cancel superseded runs

* fix(tests): poll for the new root index instead of a fixed sleep

* perf(ci): cache release target builds with sccache
2026-08-16 07:00:14 -07:00
Dmitriy Kovalenko 232288c60b chore(ci): Attempt to optimize CI build time (#782) 2026-08-15 17:40:01 -07:00
dmtrKovalenko 2c9cae2e0f chore: bump fff-mcp release artifacts to v0.10.4 2026-08-15 22:16:40 +00:00
Dmitriy Kovalenko a166255d8f chore: release 0.10.4
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-linux-android (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim aarch64-linux-android (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Build Python sdist (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
2026-08-15 14:35:20 -07:00
Kh05ifr4nD ffd8eb9c08 fix(mcp): standalone constraints in multi_grep (#753)
* fix: apply standalone constraints in multi_grep

* chore: use pub(crate)

---------

Co-authored-by: Dmitriy Kovalenko <dmitriy@iusevimbtw.com>
2026-08-14 22:11:24 -07:00
Dmitriy Kovalenko ed6b40a591 chore(deps): Update zlob to 1.6.3 (#777) 2026-08-14 21:47:10 -07:00
Dmitriy Kovalenko c4f6c600d6 fix(core): share one LMDB env per path within a process (#775)
because LMDB usee posix lock one process can not hold many open
databases
2026-08-14 21:46:46 -07:00
Gustav the Bot 787a9b1cb7 fix: rebuild file picker after FFFClearCache files (#772) (#773)
clear_cache('files') drops the Rust picker but core.lua kept
state.file_picker_initialized set, so ensure_initialized short-circuited
on state.initialized and never rebuilt it. Every later FFFScan/search then
ran against a dropped picker (watcher logs "File picker not initialized"),
which SIGSEGVs on Linux.

Split ensure_initialized: one-time setup stays gated on state.initialized,
picker creation is gated separately on state.file_picker_initialized.
clear_cache now clears that flag via mark_file_picker_uninitialized so the
next ensure_initialized rebuilds the picker.

Closes #772

Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
2026-08-14 09:10:03 -07:00
Dmitriy Kovalenko 6398d32c0c fix: Do not exit MCP if the parent process is alive (#770)
closes #703

Bumped inactivity timeout to an hour and make it actually check every
minute if parent is alive and working
2026-08-13 17:39:14 -07:00
Gustav the Bot c6194b848d fix(mcp): declare readOnlyHint on all tools (#771) (#774)
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>
2026-08-13 08:42:55 -07:00
Gustav the Bot b71b7cf4ac fix(pi-fff): cap grep total results with pageSize, clamp context (#768) (#769)
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>
2026-08-13 07:45:48 -07:00
Joe Taber cc289f0f93 chore: rename old fff.nvim repo refs (#762)
* 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.
2026-08-11 21:40:21 -07:00
Gustav the Bot 2107f038dc fix(grep): keep FilePath scope in regex/literal fallback (#756) (#764)
* 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>
2026-08-11 20:54:36 -07:00
Dmitriy Kovalenko 9441cbc5ae fix(bun): Add a proper build step for fff-bun (#766)
We never build it and this seems to be not a good idea
2026-08-11 20:45:40 -07:00
Gustav the Bot dd87489d6b fix: add missing clap attribute to no_content_indexing (#754) (#765)
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>
2026-08-11 11:28:58 -07:00
chenydev d4c416c2fc fix(pi-fff): cache sdk import across reloads to avoid hang (#758)
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>
2026-08-08 19:28:16 -07:00
dmtrKovalenko b6f351d729 chore: bump fff-mcp release artifacts to v0.10.3 2026-08-07 04:55:45 +00:00
Dmitriy Kovalenko e2cad2f09e chore: release 0.10.3
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-linux-android (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-linux-android (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Build Python sdist (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
2026-08-06 20:26:51 -07:00
Gustav the Bot 4afad820b4 fix(nvim): refuse fs-root/home index at Lua level before FFI (#745) (#748)
* 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>
2026-08-06 20:25:43 -07:00
Gustav the Bot 7af3a9dab6 fix(pi-fff): make home-dir scanning configurable, warn when indexing $HOME (#743) (#749)
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>
2026-08-06 20:25:33 -07:00
Dmitriy Kovalenko 031005e227 fix: Reduce amount of rescans in giant /Users/neogoose like folders (#751) 2026-08-06 19:47:41 -07:00
Dmitriy Kovalenko 3a0ce85c54 fix: Correctly handle empty directories during the scan (#735)
* 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
2026-08-05 20:09:48 -07:00
Gustav the Bot 695724eb4a fix(pi-fff): dedup concurrent aux finders and bound grep time (#746) (#750)
* 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>
2026-08-05 18:41:02 -07:00
Dmitriy Kovalenko 086044f5f7 fix: Gitignore incompatbility (#744)
Closes https://github.com/dmtrKovalenko/fff/issues/723 fixed in zlob
2026-08-04 21:19:14 -07:00
Gustav the Bot 3298ed9f39 fix(pi-fff): prefix promptGuidelines with tool name (#737) (#741)
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>
2026-08-04 11:28:43 -07:00
Gustav the Bot 1eb913e509 chore(deps): bump git2 to 0.21.0 to clear RUSTSEC-2026-0183/0184 (#733) (#734)
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>
2026-07-30 19:17:29 -07:00
Gustav the Bot e453d007d5 chore: regenerate Neovim vimdoc (#732)
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
2026-07-30 17:55:57 -07:00
Gustav the Bot 9033efb60f fix(fff-mcp): compare CARGO_PKG_VERSION to stable release tag (#721) (#729)
* 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>
2026-07-29 18:29:52 -07:00
Mikhail Wijanarko ea2ee6b341 fix(pi-fff): pin SDK dependencies during publish (#712)gg 2026-07-29 17:14:13 -07:00
Vinicios Lugli 84a9cd556f feat(mcp): expose home and fs root scanning options (#720)
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.
2026-07-29 11:09:19 -07:00
jadonwb 9ec35bd101 chore(readme): fix neovim borders instructions (#727)
I apologize, but in my last pr I missed this typo, it is actually 5 junction border characters and not 6.
2026-07-29 11:09:00 -07:00
Timothy John Perisho Eccleston edda2cbef8 chore(readme): fix typo in README.md (#731)
* typo in README.md

* typo in README.md
2026-07-29 11:08:29 -07:00
Dmitriy Kovalenko 2cf871210b chore(readme): Add #1 badge 2026-07-29 08:24:12 -07:00
Gustav the Bot fbee146c44 chore: regenerate Neovim vimdoc (#718)
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
2026-07-28 12:41:01 -07:00
jadonwb 6a239e9875 feat: accept custom border character arrays (#715)
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
2026-07-27 14:32:12 -07:00
Gustav the Bot fde8c52a29 chore: regenerate Neovim vimdoc (#709)
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
2026-07-23 22:09:16 -07:00
Dmitriy Kovalenko 84a9b50dd7 fix(nix): Prevent illegal instructions sneaking into nix builds (#707) 2026-07-23 16:54:14 -07:00
Dmitriy Kovalenko 63b126e7b0 fix: Not finding if needle contains != (#706) 2026-07-23 13:01:29 -07:00
Dmitriy Kovalenko 167f7d67cb chore: Fix CI (#704)
closes https://github.com/dmtrKovalenko/fff/issues/702
2026-07-23 11:49:29 -07:00
Gustav the Bot 829bfa9570 fix(pi-fff): preserve path/exclude in fuzzy grep fallback (#697) (#699)
* 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>
2026-07-23 11:38:42 -07:00
Gustav the Bot 9bab609ddf fix(pi-fff): temporarily remove aux pickers frecency db access (#700) (#701)
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>
2026-07-23 11:05:33 -07:00
dmtrKovalenko 05a35c6d48 chore: bump fff-mcp release artifacts to v0.10.1 2026-07-20 23:48:58 +00:00
Dmitriy Kovalenko 686a84959d chore: release 0.10.1
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-linux-android (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-linux-android (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Has been cancelled
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Build Python sdist (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
2026-07-20 16:05:07 -07:00
Gustav the Bot 11a0d447a6 chore(node & bun): add Android arm64 (Termux) support (#695)
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>
2026-07-20 15:17:17 -07:00
ziqian li 9dee049973 fix(pi-fff): handle Windows cross-volume external paths (#684)
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.
2026-07-20 14:45:10 -07:00
Gustav the Bot 16a666b095 fix(pi-fff): install fff-bun so bun runtimes can load the SDK (#689) (#694)
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>
2026-07-20 11:44:34 -07:00
Anton Panasenko 073698c8e7 fix(mcp): use absolute path for Codex setup (#685) 2026-07-17 13:27:45 -07:00
dmtrKovalenko b14c31d137 chore: bump fff-mcp release artifacts to v0.10.0 2026-07-16 20:28:10 +00:00
136 changed files with 11586 additions and 5859 deletions
+36 -17
View File
@@ -18,6 +18,14 @@ env:
# Force Node 24 for all JS-based actions to avoid the libuv
# process_title assertion crash on Windows (known Node 20 bug).
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# e2e only needs a working binary, so skip fat LTO (same settings as the `ci`
# profile releases ship). Overriding release keeps artifacts in target/release.
CARGO_PROFILE_RELEASE_LTO: thin
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
lua-tests:
@@ -25,6 +33,13 @@ jobs:
runs-on: ${{ matrix.os }}
# e2e tests could be flaky on CI so we do not block release creation if they failed
continue-on-error: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
# alpine-musl installs rust via apk and has no sccache, so keep this scoped.
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
# fff-search alone exceeds the 600s default on windows, and the server
# sees no new requests while it compiles, so it would idle out mid-unit.
SCCACHE_IDLE_TIMEOUT: "0"
strategy:
fail-fast: false
matrix:
@@ -32,7 +47,6 @@ jobs:
- os: ubuntu-latest
- os: macos-latest
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
@@ -49,20 +63,23 @@ jobs:
cache-on-failure: false
cache-key: "v2-lua-e2e"
rustflags: ""
target: ${{ matrix.target || '' }}
- name: Build Rust binary (Windows)
if: matrix.target
run: cargo build --release --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Copy binary to target/release (Windows)
if: matrix.target
- name: Build Rust binary
shell: bash
run: |
cp target/${{ matrix.target }}/release/fff_nvim.dll target/release/fff_nvim.dll
run: make build-e2e
- name: sccache stats
if: always()
shell: bash
run: sccache --show-stats
- name: Verify Windows DLL has no unexpected dependencies
if: matrix.target
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
# Find dumpbin via vswhere (always available on GitHub Actions Windows runners)
@@ -78,10 +95,6 @@ jobs:
exit 1
}
- name: Build Rust binary
if: ${{ !matrix.target }}
run: make build
- name: Install Neovim
uses: rhysd/action-setup-vim@v1
with:
@@ -135,7 +148,7 @@ jobs:
- name: Install node dependencies
shell: bash
run: cd packages/fff-node && npm install
run: cd packages && npm ci
- name: Run node tests
shell: bash
@@ -185,12 +198,14 @@ jobs:
run: cargo build --release -p fff-c
- name: Install workspace npm deps
run: npm install --no-package-lock
working-directory: packages
run: npm ci
# Upstream @yuuang/ffi-rs-linux-x64-musl ships with libc:"glibc" in
# its package.json (a publishing bug in ffi-rs), so npm filters it
# out. Force-install it so the FFI runtime is present on Alpine.
- name: Install ffi-rs musl runtime
working-directory: packages
run: |
FFI_RS_VERSION=$(node -p "require('ffi-rs/package.json').version")
npm install --no-package-lock --no-save --force \
@@ -201,13 +216,17 @@ jobs:
# this is what exercises detectLinuxLibc().
- name: Stage musl bin package
run: |
PKG_DIR=node_modules/@ff-labs/fff-bin-linux-x64-musl
PKG_DIR=packages/node_modules/@ff-labs/fff-bin-linux-x64-musl
mkdir -p "$PKG_DIR"
cp target/release/libfff_c.so "$PKG_DIR/libfff_c.so"
cat >"$PKG_DIR/package.json" <<'JSON'
{ "name": "@ff-labs/fff-bin-linux-x64-musl", "version": "0.0.0" }
JSON
# fff-node builds with bun; the npm `bun` package ships musl binaries
- name: Install bun
run: npm install -g bun
- name: Build fff-node
working-directory: packages/fff-node
run: npm run build
+4
View File
@@ -12,6 +12,10 @@ on:
- '**.md'
- 'doc/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
lua-ls:
name: lua-language-server type check
+4
View File
@@ -12,6 +12,10 @@ on:
- '**.md'
- 'doc/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
check:
runs-on: ubuntu-22.04
+46
View File
@@ -0,0 +1,46 @@
name: Oxc
permissions:
contents: read
on:
push:
branches:
- main
paths:
- "packages/**"
- .github/workflows/oxc.yml
pull_request:
paths:
- "packages/**"
- .github/workflows/oxc.yml
env:
CLICOLOR: 1
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
oxc:
name: Check packages with oxfmt and oxlint
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
- name: Install
run: bun install --frozen-lockfile
- name: oxfmt
run: bun run format:check
- name: oxlint
run: bun run lint -- -f github --deny-warnings
+24 -2
View File
@@ -15,6 +15,17 @@ on:
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "13.0"
CARGO_PROFILE_RELEASE_LTO: thin
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
# fff-search alone exceeds the 600s default on windows, and the server sees
# no new requests while it compiles, so it would idle out mid-unit.
SCCACHE_IDLE_TIMEOUT: "0"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
test:
@@ -31,7 +42,16 @@ jobs:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rust-lang/setup-rust-toolchain@v1.15.4
with:
cache: true
cache-on-failure: true
cache-key: "v1-rust-python"
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Setup uv
uses: astral-sh/setup-uv@v5
@@ -43,6 +63,8 @@ jobs:
working-directory: packages/fff-python
shell: bash
run: |
uv sync --all-extras
# --no-install-project: uv would otherwise build the wheel via
# maturin's PEP 517 backend, then `maturin develop` builds it again.
uv sync --all-extras --no-install-project
uv run maturin develop --release
uv run pytest -v
+53 -12
View File
@@ -2,7 +2,7 @@ name: Build & Publish
on:
push:
branches: [main, fix/use-trusted-publishing]
branches: [main, fix/npm-pulish]
tags:
- "v*"
pull_request:
@@ -17,10 +17,18 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
build-nvim:
name: Build Neovim ${{ matrix.target }}
runs-on: ${{ matrix.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
SCCACHE_IDLE_TIMEOUT: "0"
permissions:
contents: read
id-token: write
@@ -91,6 +99,11 @@ jobs:
with:
key: nvim-${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Install Zig
uses: mlugg/setup-zig@v2
with:
@@ -146,6 +159,10 @@ jobs:
build-c:
name: Build C FFI ${{ matrix.target }}
runs-on: ${{ matrix.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
SCCACHE_IDLE_TIMEOUT: "0"
permissions:
contents: read
strategy:
@@ -183,6 +200,7 @@ jobs:
- os: ubuntu-latest
target: aarch64-linux-android
artifact_name: target/aarch64-linux-android/ci/libfff_c.so
npm_package: fff-bin-android-arm64
lib_filename: libfff_c.so
ext: so
@@ -227,6 +245,11 @@ jobs:
with:
key: c-${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Install Zig
uses: mlugg/setup-zig@v2
with:
@@ -273,7 +296,6 @@ jobs:
mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}"
- name: Prepare npm package
if: "!contains(matrix.target, 'android')"
shell: bash
run: |
# Copy the built binary into the platform npm package directory
@@ -286,7 +308,6 @@ jobs:
path: c-lib-${{ matrix.target }}.*
- name: Upload npm package artifact
if: "!contains(matrix.target, 'android')"
uses: actions/upload-artifact@v4
with:
name: npm-${{ matrix.npm_package }}
@@ -295,6 +316,10 @@ jobs:
build-mcp:
name: Build MCP ${{ matrix.target }}
runs-on: ${{ matrix.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
SCCACHE_IDLE_TIMEOUT: "0"
permissions:
contents: read
strategy:
@@ -345,6 +370,11 @@ jobs:
with:
key: mcp-${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Install Zig
uses: mlugg/setup-zig@v2
with:
@@ -479,11 +509,14 @@ jobs:
name: Release
needs: [build-nvim, build-c, build-mcp, build-python, build-python-sdist]
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v'))
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
permissions:
contents: write
steps:
# we have to make sure that pushing a commit on this workflow triggers the CI for nightly neovim
- uses: actions/checkout@v5
with:
token: ${{ secrets.GUSTAV_PAT || github.token }}
- name: Install Lua
uses: leafo/gh-actions-lua@v12
@@ -610,7 +643,7 @@ jobs:
Update mcp via:
```sh
curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.sh | bash
curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff/main/install-mcp.sh | bash
```
- name: Bump Homebrew formula (uses local checksums)
@@ -622,8 +655,6 @@ jobs:
run: make bump-install-mcp-sh VERSION="${{ steps.version.outputs.version }}" BINARIES_DIR=./binaries
- name: Commit formula + installer bump to main
# Uses the default GITHUB_TOKEN configured by actions/checkout above.
# Requires github-actions[bot] in the main branch-protection bypass list.
if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko'
uses: stefanzweifel/git-auto-commit-action@v5
with:
@@ -638,7 +669,7 @@ jobs:
name: Publish Python wheels to PyPI
needs: [build-python, build-python-sdist]
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v'))
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
environment:
name: pypi
url: https://pypi.org/p/fff-search
@@ -663,7 +694,7 @@ jobs:
name: Publish Rust crates
needs: [build-nvim, build-c, build-mcp]
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v'))
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
permissions:
contents: read
id-token: write
@@ -694,7 +725,7 @@ jobs:
name: Publish npm packages
needs: [build-c]
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v'))
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/npm-pulish' || startsWith(github.ref, 'refs/tags/v'))
permissions:
contents: read
id-token: write
@@ -710,6 +741,8 @@ jobs:
node-version: "25"
registry-url: "https://registry.npmjs.org"
- uses: oven-sh/setup-bun@v2
- name: Determine version
id: version
run: lua scripts/determine-version.lua
@@ -738,6 +771,16 @@ jobs:
fi
done
# Every install must happen before the first set-npm-version: the bump
# injects platform optionalDependencies that only resolve once published,
# and npm ci validates the whole workspace against the lockfile.
- name: Install and build JS packages
working-directory: packages
run: |
npm ci
(cd fff-bun && npm run build)
(cd fff-node && npm run build)
- name: Publish bun package
run: |
VERSION="${{ steps.version.outputs.version }}"
@@ -758,8 +801,6 @@ jobs:
make set-npm-version PKG=packages/fff-node VERSION="$VERSION"
cd packages/fff-node
npm install
npm run build
npm publish --tag "$TAG" --access public --provenance
- name: Publish pi-fff package
+39 -2
View File
@@ -17,6 +17,15 @@ env:
# Ensure consistent macOS deployment target across all compiled objects
# (Rust, cc-compiled C code, and Zig-compiled zlob) to avoid linker warnings
MACOSX_DEPLOYMENT_TARGET: "13"
# RUSTC_WRAPPER is set per job, since cargo fmt runs without sccache.
SCCACHE_GHA_ENABLED: "true"
# fff-search alone exceeds the 600s default on windows, and the server sees
# no new requests while it compiles, so it would idle out mid-unit.
SCCACHE_IDLE_TIMEOUT: "0"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
test:
@@ -46,8 +55,16 @@ jobs:
cache-key: "v1-rust"
components: rustfmt, clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Run tests
run: cargo test --no-default-features --features zlob --workspace --exclude fff-nvim
env:
RUSTC_WRAPPER: sccache
# fff-python requires full python o3 machinery which is very slow
run: cargo test --no-default-features --features zlob --workspace --exclude fff-nvim --exclude fff-python
stress-test:
name: Fuzz Tests
@@ -65,6 +82,7 @@ jobs:
FFF_STRESS_CASES: "5"
FFF_STRESS_MIN_OPS: "30"
FFF_STRESS_MAX_OPS: "60"
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@v5
@@ -81,6 +99,11 @@ jobs:
cache-key: "v1-rust-stress-${{ matrix.os }}"
components: rustfmt, clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Stress test seeded
shell: bash
run: make test-stress-seeded
@@ -124,7 +147,14 @@ jobs:
cache-on-failure: true
cache-key: "v1-rust-i686"
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Build fff-search for i686
env:
RUSTC_WRAPPER: sccache
run: cargo build -p fff-search --target i686-unknown-linux-gnu
fmt:
@@ -157,6 +187,13 @@ jobs:
with:
toolchain: stable
components: clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: v0.17.0
- name: Run clippy
env:
RUSTC_WRAPPER: sccache
run: cargo clippy --no-default-features --features zlob -- -D warnings
+4
View File
@@ -12,6 +12,10 @@ on:
env:
CLICOLOR: 1
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
spelling:
name: Spell Check with Typos
+4
View File
@@ -20,6 +20,10 @@ on:
env:
CLICOLOR: 1
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
jobs:
stylua:
name: Check lua files using Stylua
+1 -1
View File
@@ -1,4 +1,4 @@
-- luacheck configuration for fff.nvim
-- luacheck configuration for fff
-- https://luacheck.readthedocs.io/en/stable/config.html
-- Neovim globals
Generated
+15 -14
View File
@@ -633,7 +633,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fff-c"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"fff-query-parser",
"fff-search",
@@ -643,7 +643,7 @@ dependencies = [
[[package]]
name = "fff-grep"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"bstr",
"memchr",
@@ -651,7 +651,7 @@ dependencies = [
[[package]]
name = "fff-mcp"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"clap",
"fff-query-parser",
@@ -662,8 +662,10 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
@@ -682,7 +684,7 @@ dependencies = [
[[package]]
name = "fff-nvim"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"ahash",
"chrono",
@@ -703,7 +705,7 @@ dependencies = [
[[package]]
name = "fff-python"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"fff-query-parser",
"fff-search",
@@ -713,7 +715,7 @@ dependencies = [
[[package]]
name = "fff-query-parser"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"criterion",
"zlob",
@@ -721,7 +723,7 @@ dependencies = [
[[package]]
name = "fff-search"
version = "0.10.0"
version = "0.10.5"
dependencies = [
"ahash",
"aho-corasick",
@@ -925,15 +927,14 @@ dependencies = [
[[package]]
name = "git2"
version = "0.20.4"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e"
dependencies = [
"bitflags 2.11.0",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
@@ -1333,9 +1334,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libgit2-sys"
version = "0.18.3+1.9.2"
version = "0.18.7+1.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89"
dependencies = [
"cc",
"libc",
@@ -3236,9 +3237,9 @@ dependencies = [
[[package]]
name = "zlob"
version = "1.6.1"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e41cb327ac1b7e7e0d4514658500cb5734cd655edbe4e5ffeda69955da9028ee"
checksum = "f0f010d9b10495cc0037cb47cedc143c39c7fa2d59f752dca19036fd0234a210"
dependencies = [
"bindgen",
"bitflags 2.11.0",
+4 -4
View File
@@ -15,8 +15,8 @@ resolver = "2"
module_inception = "allow"
[workspace.dependencies]
fff-grep = { version = "0.10.0", path = "crates/fff-grep" }
fff-query-parser = { version = "0.10.0", path = "crates/fff-query-parser", default-features = false }
fff-grep = { version = "0.10.5", path = "crates/fff-grep" }
fff-query-parser = { version = "0.10.5", path = "crates/fff-query-parser", default-features = false }
# Shared dependencies
ahash = "0.8"
@@ -26,7 +26,7 @@ ctrlc = "3.4.2"
dirs = "5.0"
dunce = "1.0"
# git2 - base config without TLS (each crate adds platform-specific TLS)
git2 = { version = "0.20.2", default-features = false, features = [
git2 = { version = "0.21.0", default-features = false, features = [
"vendored-libgit2",
] }
glidesort = "0.1"
@@ -36,7 +36,7 @@ ignore = "0.4.22"
memmap2 = "0.9"
mimalloc = "0.1.47"
signal-hook-registry = "1.4"
zlob = { version = "=1.6.1" }
zlob = { version = "=1.6.3" }
mlua = { version = "0.11.1", features = ["module", "luajit"] }
neo_frizbee = { version = "0.11.0", features = ["match_end_col"] }
+8 -8
View File
@@ -2,34 +2,34 @@
# Maintained in-repo; auto-bumped by .github/workflows/release.yaml on stable releases.
class FffMcp < Formula
desc "Fast file search toolkit for AI agents (MCP server)"
homepage "https://github.com/dmtrKovalenko/fff.nvim"
homepage "https://github.com/dmtrKovalenko/fff"
license "MIT"
version "0.9.6"
version "0.10.5"
LIVECHECK_REPO = "dmtrKovalenko/fff.nvim".freeze
RELEASE_BASE = "https://github.com/dmtrKovalenko/fff.nvim/releases/download".freeze
LIVECHECK_REPO = "dmtrKovalenko/fff".freeze
RELEASE_BASE = "https://github.com/dmtrKovalenko/fff/releases/download".freeze
on_macos do
on_arm do
url "#{RELEASE_BASE}/v#{version}/fff-mcp-aarch64-apple-darwin"
sha256 "29a7fadeafb062f3e5954b1ab8c69e14dca24f5e061cd8d3b1ea1bab385a3754"
sha256 "2f1a75c2401e5df7f5a10827f9ce9fe8401f290ff99c3f3f1423c7d7b156d849"
end
on_intel do
url "#{RELEASE_BASE}/v#{version}/fff-mcp-x86_64-apple-darwin"
sha256 "58259324c2c13a1b6f24f13138c2cd3eae9ff20e05201a539beb8f2044a651aa"
sha256 "c81e22a7fb657731a7441be877c622a110eb7e7dbc94e3b57d79c9cea91f38db"
end
end
on_linux do
on_arm do
url "#{RELEASE_BASE}/v#{version}/fff-mcp-aarch64-unknown-linux-gnu"
sha256 "91e6fa14e040588dc92de854e35020536f1e2458ce3386b2b727b2e7a88f6684"
sha256 "48f5af0d9e6ef4506c5cccd3f4571da4f0acc9b9c0967832167e49fe651f8ee5"
end
on_intel do
url "#{RELEASE_BASE}/v#{version}/fff-mcp-x86_64-unknown-linux-gnu"
sha256 "d1bd2b89a79e8eda71b1754260499cec1feaafd2adf372e92371c8d6b68509a3"
sha256 "0408cbac06303b3bf2041181089d8b4974b527883242bdcf8ba599b41c003973"
end
end
+35 -21
View File
@@ -14,7 +14,7 @@ SHELL := bash
# string rather than the literal `-o` / `pipefail` tokens.
.SHELLFLAGS := -o pipefail -euc
.PHONY: build build-c-lib install uninstall test test-rust test-c-smoke test-c-api test-lua test-lua-snap test-version test-bun test-node prepare-bun prepare-bun-packaged prepare-node set-npm-version header test-stress test-stress-seeded test-stress-random test-stress-regressions test-stress-repos test-node-stress sync-js-api sync-js-api-check bump-homebrew-formula bump-install-mcp-sh test-bun-compile
.PHONY: build build-c-lib install uninstall test test-rust test-rescan test-rescan-known-defects rescan-probe test-c-smoke test-c-api test-lua test-lua-snap test-version test-bun test-node prepare-bun prepare-bun-packaged prepare-node set-npm-version header test-stress test-stress-seeded test-stress-random test-stress-regressions test-stress-repos test-node-stress sync-js-api sync-js-api-check bump-homebrew-formula bump-install-mcp-sh test-bun-compile
all: format test lint
@@ -45,6 +45,11 @@ sync-js-api-check:
build:
cargo build --release --no-default-features --features zlob
# Only the crates the e2e suites load (nvim lua tests + C/bun/node FFI tests),
# skipping fff-python (pyo3) and fff-mcp (tokio/rmcp) which e2e never touches.
build-e2e:
cargo build --release -p fff-nvim -p fff-c --no-default-features --features zlob
build-c-lib:
cargo build --release -p fff-c --no-default-features --features zlob
@@ -90,7 +95,26 @@ test-setup:
fi
test-rust:
cargo test --workspace --no-default-features --features zlob --exclude fff-nvim
cargo test --workspace --no-default-features --features zlob --exclude fff-nvim --exclude fff-python
# Watcher rescan harness: asserts that editing, build output, git activity and
# preview reads all stay on the incremental path instead of re-walking the tree.
test-rescan:
cargo test -p fff-search --no-default-features --features zlob \
--lib --test rescan_regression -- rescan
# Live probe for watcher rescan requests and their causes.
# Usage: make rescan-probe DIR=~/some/repo [SECONDS=120]
rescan-probe:
cargo run --release -p fff-nvim --bin rescan_probe \
--no-default-features --features zlob,rescan-stats -- \
$(or $(DIR),.) $(if $(SECONDS),--seconds $(SECONDS),)
# The same harness, restricted to cases that currently fail on purpose. Each
# `#[ignore]` reason names the code that causes the unnecessary rescan.
test-rescan-known-defects:
cargo test --no-fail-fast -p fff-search --no-default-features --features zlob \
--lib --test rescan_regression -- --ignored --nocapture
CC ?= cc
CFLAGS ?= -O0 -g -Wall -Wextra -std=c99
@@ -99,7 +123,7 @@ SMOKE_BIN := $(TARGET_DIR)/fff_c_smoke
SMOKE_SRC := crates/fff-c/tests/smoke.c
SMOKE_INCLUDE := crates/fff-c/include
test-c-smoke: build
test-c-smoke: build-e2e
$(CC) $(CFLAGS) -I $(SMOKE_INCLUDE) -L $(TARGET_DIR) \
-Wl,-rpath,@loader_path/../target/release \
-Wl,-rpath,$$(pwd)/$(TARGET_DIR) \
@@ -112,7 +136,7 @@ test-c-api: test-c-smoke
# neovim instance swallows internal crashes and doesn't rise the the error exiting silently
# so check the stdout in case the sigsegv coming out of fff was printed (actual regression).
# Output is streamed live via `tee`; pipefail (set above) propagates nvim's exit.
test-lua: test-setup build
test-lua: test-setup build-e2e
@logfile=$$(mktemp); \
trap 'rm -f "$$logfile"' EXIT; \
nvim --headless -u tests/minimal_init.lua \
@@ -124,7 +148,7 @@ test-lua: test-setup build
exit 1; \
fi
test-lua-snap: test-setup build
test-lua-snap: test-setup build-e2e
@logfile=$$(mktemp); \
trap 'rm -f "$$logfile"' EXIT; \
nvim --headless -u tests/minimal_init.lua \
@@ -140,13 +164,13 @@ test-version: test-setup
nvim --headless -u tests/minimal_init.lua \
-c "PlenaryBustedFile tests/version_spec.lua" 2>&1
prepare-bun: build sync-js-api
prepare-bun: build-e2e sync-js-api
mkdir -p packages/fff-bun/bin
cp target/release/libfff_c.dylib packages/fff-bun/bin/ 2>/dev/null || true; \
cp target/release/libfff_c.so packages/fff-bun/bin/ 2>/dev/null || true; \
cp target/release/fff_c.dll packages/fff-bun/bin/ 2>/dev/null || true
prepare-node: build sync-js-api
prepare-node: build-e2e sync-js-api
mkdir -p packages/fff-node/bin
cp target/release/libfff_c.dylib packages/fff-node/bin/ 2>/dev/null || true; \
cp target/release/libfff_c.so packages/fff-node/bin/ 2>/dev/null || true; \
@@ -250,17 +274,7 @@ test-stress: test-stress-seeded test-stress-random test-stress-regressions test-
set-npm-version:
@test -n "$(PKG)" || (echo "PKG is required" && exit 1)
@test -n "$(VERSION)" || (echo "VERSION is required" && exit 1)
node -e " \
const fs = require('fs'); \
const pkg = JSON.parse(fs.readFileSync('$(PKG)/package.json', 'utf8')); \
pkg.version = '$(VERSION)'; \
if (pkg.optionalDependencies) { \
for (const dep of Object.keys(pkg.optionalDependencies)) { \
pkg.optionalDependencies[dep] = '$(VERSION)'; \
} \
} \
fs.writeFileSync('$(PKG)/package.json', JSON.stringify(pkg, null, 2) + '\n'); \
"
node scripts/set-npm-version.mjs "$(PKG)" "$(VERSION)"
@echo "Set $(PKG) to $(VERSION)"
format-rust:
@@ -268,7 +282,7 @@ format-rust:
format-lua:
stylua .
format-ts:
bun format
cd packages && bun format
format: format-rust format-lua format-ts
@@ -277,13 +291,13 @@ lint-rust:
lint-lua:
~/.luarocks/bin/luacheck .
lint-ts:
bun lint
cd packages && bun lint
lint: lint-rust lint-lua lint-ts
check: format lint
FFF_RELEASE_REPO ?= dmtrKovalenko/fff.nvim
FFF_RELEASE_REPO ?= dmtrKovalenko/fff
FFF_FORMULA_PATH ?= Formula/fff-mcp.rb
FFF_INSTALL_SCRIPT_PATH ?= install-mcp.sh
+46 -11
View File
@@ -4,11 +4,14 @@
<i>A file search toolkit for humans and AI agents. Really fast.</i>
</p>
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.
Typo-resistant path and content search, frequency-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](http://github.com/anomalyco/opencode/), [nushell](https://github.com/nushell/nushell), and many more amazing projects!
Originally started as [Neovim plugin](#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.
<p>
<a href="https://trendshift.io/repositories/26711?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-26711" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/26711" alt="dmtrKovalenko%2Ffff | Trendshift" width="250" height="55"/></a>
</p>
---
@@ -34,7 +37,7 @@ curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash
Windows (PowerShell):
```powershell
irm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex
irm https://raw.githubusercontent.com/dmtrKovalenko/fff/main/install-mcp.ps1 | iex
```
The scripts live at [`install-mcp.sh`](./install-mcp.sh) and [`install-mcp.ps1`](./install-mcp.ps1) if you want to read them first. They print the exact wiring instructions for your client.
@@ -46,7 +49,32 @@ brew install dmtrKovalenko/fff/fff-mcp
brew upgrade fff-mcp # after new stable releases
```
Formula lives in [`Formula/fff-mcp.rb`](./Formula/fff-mcp.rb) in this repo and is **auto-bumped on every stable release** (see `bump-homebrew-formula` in [`.github/workflows/release.yaml`](./.github/workflows/release.yaml)). Installs the prebuilt `fff-mcp` binary from [GitHub releases](https://github.com/dmtrKovalenko/fff.nvim/releases).
Formula lives in [`Formula/fff-mcp.rb`](./Formula/fff-mcp.rb) in this repo and is **auto-bumped on every stable release** (see `bump-homebrew-formula` in [`.github/workflows/release.yaml`](./.github/workflows/release.yaml)). Installs the prebuilt `fff-mcp` binary from [GitHub releases](https://github.com/dmtrKovalenko/fff/releases).
### Codex setup
Register the installed binary using its absolute path, since Codex desktop sessions may not inherit your interactive shell's `PATH`.
Homebrew:
```bash
codex mcp add fff -- "$(brew --prefix)/bin/fff-mcp"
```
One-line installer:
```bash
codex mcp add fff -- "$HOME/.local/bin/fff-mcp"
```
This creates an entry in `~/.codex/config.toml` similar to:
```toml
[mcp_servers.fff]
command = "/opt/homebrew/bin/fff-mcp"
```
Use the actual installed path for your system, then restart Codex or start a new task so it loads the server.
Once the server is connected, ask the agent to "use fff" and it picks up the `ffgrep`, `fffind`, and `fff-multi-grep` tools.
@@ -92,7 +120,7 @@ Three operating modes, switchable at runtime with `/fff-mode`:
| `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`.
Env vars: `PI_FFF_MODE`, `FFF_FRECENCY_DB`, `FFF_HISTORY_DB`. Flags: `--fff-mode`, `--fff-frecency-db`, `--fff-history-db`. The databases default to your existing fff.nvim ones when present, otherwise `~/.pi/agent/fff/`.
### Agent-facing tools
@@ -125,8 +153,9 @@ https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb
#### lazy.nvim
```lua
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:Lazy clean`
{
'dmtrKovalenko/fff.nvim',
'dmtrKovalenko/fff',
build = function()
-- downloads a prebuilt binary or falls back to cargo build
require("fff.download").download_or_build_binary()
@@ -159,13 +188,14 @@ https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb
#### vim.pack
```lua
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:packdel fff.nvim`
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff' })
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
if name == 'fff' and (kind == 'install' or kind == 'update') then
if not ev.data.active then vim.cmd.packadd('fff') end
require('fff.download').download_or_build_binary()
end
end,
@@ -284,6 +314,11 @@ require('fff').setup({
-- 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'
-- border = {
-- { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' },
-- { ' ', ' ', ' ', ' ', ' ' },
-- },
flex = { size = 130, wrap = 'top' },
min_list_height = 10, -- do not display anything except the list below this threshold
show_scrollbar = true,
@@ -400,7 +435,7 @@ Both find and grep accept these tokens to refine a query:
- `git:modified`. One of `modified`, `staged`, `deleted`, `renamed`, `untracked`, `ignored`.
- `test/`. Any deeply nested children of `test/`.
- `!something`, `!test/`, `!git:modified`. Exclusion.
- `!something`, `!test/`, `!git:modified`. Exclusion. Text exclusions need at least 3 alphanumeric-containing characters, so operators like `!=` or `!==` work.
- `./**/*.{rs,lua}`. Any valid glob, powered by [zlob](https://github.com/dmtrKovalenko/zlob).
Grep-only:
@@ -581,7 +616,7 @@ cargo build --release -p fff-c --features zlob
The output is a `cdylib` (`libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`). The header lives at [`crates/fff-c/include/fff.h`](./crates/fff-c/include/fff.h).
Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/dmtrKovalenko/fff.nvim/releases). The same binaries also ship inside the `@ff-labs/fff-bin-*` npm packages.
Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/dmtrKovalenko/fff/releases). The same binaries also ship inside the `@ff-labs/fff-bin-*` npm packages.
### Install
@@ -758,7 +793,7 @@ FFF is a file search library, not a CLI. Ripgrep and fzf are great tools, but th
FFF keeps the index and the file cache resident in one long-lived process and exposes the same Rust core through four thin layers: a native crate (`fff-search`), a C library (`libfff_c`), a Node/Bun SDK (`@ff-labs/fff-node`), and an MCP server. You call `FileFinder.create()` once, then every subsequent search hits warm memory. On a 500k-file Chromium checkout, that is the difference between 3-9 **SECONDS** per ripgrep spawn and sub-10 ms per FFF query.
Algorithm for fuzzy matching is much more comprehensive than fzf's algorithm it is **typo-resistant** and we provide a query language with additional constraint parsing for prefiltering e.g. "\*.rs !test/ shcema" is a perfectly valid query for fff, but fzf wouldn't find anything even for a single typo in "shcema".
Algorithm for fuzzy matching is much more comprehensive than fzf's algorithm. It is **typo-resistant** and we provide a query language with additional constraint parsing for prefiltering e.g. "\*.rs !test/ shcema" is a perfectly valid query for fff, but fzf wouldn't find anything even for a single typo in "shcema".
### Why a programmatic API matters
-35
View File
@@ -1,35 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"files": {
"includes": ["packages/**/*.ts", "!packages/*/dist"],
"ignoreUnknown": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 90
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"complexity": {
"noForEach": "off"
}
}
}
}
-421
View File
@@ -1,421 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"devDependencies": {
"@biomejs/biome": "^2.4.4",
},
},
"packages/fff-bun": {
"name": "@ff-labs/fff-bun",
"version": "0.1.37",
"bin": {
"fff-demo": "./examples/search.ts",
"fff-grep": "./examples/grep.ts",
},
"devDependencies": {
"@types/bun": "^1.3.8",
"typescript": "^5.0.0",
},
"optionalDependencies": {
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
"@ff-labs/fff-bin-win32-arm64": "0.0.0",
"@ff-labs/fff-bin-win32-x64": "0.0.0",
},
},
"packages/fff-node": {
"name": "@ff-labs/fff-node",
"version": "0.1.37",
"dependencies": {
"ffi-rs": "^1.0.0",
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.0.0",
},
"optionalDependencies": {
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
"@ff-labs/fff-bin-win32-arm64": "0.0.0",
"@ff-labs/fff-bin-win32-x64": "0.0.0",
},
},
"packages/pi-fff": {
"name": "@ff-labs/pi-fff",
"version": "0.6.0",
"dependencies": {
"@ff-labs/fff-node": "*",
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.0.0",
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*",
"@ff-labs/fff-bun": "*",
"@sinclair/typebox": "*",
},
"optionalPeers": [
"@ff-labs/fff-bun",
],
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="],
"@aws-sdk/core": ["@aws-sdk/core@3.974.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.11", "@aws-sdk/xml-builder": "^3.972.28", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JDYCPI0j7zGrzXTDFsLB346cxss7J/AxH7+O0MzWlqppJBEyB9Qe6TQXRL6iwLUo/xZkNv9KFmBL2hqElmwW0g=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.44", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-3hKJVrZ7bqXzDAXCQp+OaQ1ASN+vWstaNuEH418wQVl//cRZhqhfR9Bjk1qIWmgUGe8/D3gdO73PgidRj378EQ=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-VhwC9pGAZHhiQ2xSViyOPDFqvr9aRxGCAXZtADsUhU3R65nad7y//CwynE6mQnWNR+suRlqE79W36IVayL+m1g=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/credential-provider-env": "^3.972.44", "@aws-sdk/credential-provider-http": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.49", "@aws-sdk/credential-provider-process": "^3.972.44", "@aws-sdk/credential-provider-sso": "^3.972.49", "@aws-sdk/credential-provider-web-identity": "^3.972.49", "@aws-sdk/nested-clients": "^3.997.17", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-09Xi6ovxiK42+De/qBGF71sT5F2bWgYM+1fFyDwSOpy1xpsQ5R/naIu7MVDpH6Dic36QNc8dAv4KADtMGK2JYg=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/nested-clients": "^3.997.17", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-EfJF/1Fh9mI4pZyoheU2RY9xUhTcugIZNkD63+orXMkYj/QXacJNbKVDUK90Yv5hE+aX+rt9J/EZ9Qr3vKOa7g=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.52", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.44", "@aws-sdk/credential-provider-http": "^3.972.46", "@aws-sdk/credential-provider-ini": "^3.972.50", "@aws-sdk/credential-provider-process": "^3.972.44", "@aws-sdk/credential-provider-sso": "^3.972.49", "@aws-sdk/credential-provider-web-identity": "^3.972.49", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-7QX+PbyiWBEOVipJq8Nke/TqXT6lAPLE7fvTaopa39/IVWuLfS+Fzdy71sZJONf/mLGgmtj6aU17+REw3+aRrw=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.44", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-V+UUhZpRP7QDRhi+qgBDisM9tUBnYmMje8Bk77A6MZsfeGeGdMsQXmaHP1CDYFcept0o/Rz5g2Y0TMeVlG9dzg=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/nested-clients": "^3.997.17", "@aws-sdk/token-providers": "3.1063.0", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-9QqOYGuh5tZ76OzaT68kwI78AH+5lS/uZGGvkfxb3fc8FzRrIz2jOufNTliEBEeSAwmgK2rWLNsK+IB3zbtNPA=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/nested-clients": "^3.997.17", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-IYx1lN38MnnPXv+NBLpuATu0cZakbZ321TAfjW+aVkw7HIJF38YnEwdeEO55MSl3pl7hIX1IvvnD6EmnAzmAJw=="],
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.20", "", { "dependencies": { "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-qr/S1iFCDIXlZwlZPaCqjKcHbJFr9scIFUhbh2+SrwPXZvRhyOUWjVDJpp8xoU4qrrMR0PqK1Yw5C2sSj7xAyw=="],
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-KR2Gdui/QLbkdG9FxW3vk/vIa8KiDP5vQBNERo7MmlPHjn23GXJ53Cq5P/ok7/ALbTUiYZ78DiBHoDcvzPWvgQ=="],
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-foM3KvxGBHY9lRIm6C9JJJ5haodtXfJPPgJQcv5/c4A2pN4I7tlnOjh1o2d8Il1Y/j6GWOw3YeIYc2/VYjtGVQ=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.17", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.18", "@aws-sdk/signature-v4-multi-region": "^3.996.32", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-lDRgraoTfKRawUyc176Ow93mrNrOho/x+EoK4C+lKU+vKkHWhNhzvSMVAx0WEJUJoeQxxDN5ZdKMfiGEyNejig=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.32", "", { "dependencies": { "@aws-sdk/types": "^3.973.11", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-llvApLcsWtmRFhG2wT3WIp1CmDeRaIYutqty1ZZXoMzK7TiJ6MOLOimk9eXUS8PwgG4ew4pa4QAbt0lfhn++1w=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.11", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-YjS0qFuECClRh4qhEyW8XagW0fwEPBeZ1cfsW/gU73Kh/ExFILxbzxOfPCmzF/2DwEvhvsHYt0b0qnvStwKYrg=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.6", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZfHjfwSzeXj+Lg9AK5ZNmeDkXev6V+w2tn1t4kgDdRtUaRCthepTQiFwbD06EF9oNGH4LaLg+Mb6U16Ypv5bSw=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.28", "", { "dependencies": { "@smithy/types": "^4.14.3", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-lI/l3c/vPvsxmspzV63NfS3x9q4CkMmdhJy4QiM+NThAufVkDvi/PZZQ6xETnICL0UD7jI808pY83gllf86RFg=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="],
"@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.78.1", "", { "dependencies": { "@earendil-works/pi-ai": "^0.78.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-oPwVRkkAvyKPWyM7E4k+EaTNmynbYn7ZLG/LBh9BUnMNb2gvpMp+VQ420R6JCJ20uogSqrHnWTyosSa/rU8lVw=="],
"@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.78.1", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-CM2pkTs1iupG/maw381lC9Q/Y/aQaMGK7GILc28ttImD0ci3LDwKroDsGkWbly5JIy3iqxdRxB9JlG7vvzCzTg=="],
"@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.78.1", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.78.1", "@earendil-works/pi-ai": "^0.78.1", "@earendil-works/pi-tui": "^0.78.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-Syjf6Ib8UoY5t9ZdKjp0BRrQZuFkFBc8j2KEU9zG/ZnmYPcAxYeioofdv2Q3MEXnHEX2U8sKQptkSnJIdMsd0g=="],
"@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.78.1", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "15.0.12" } }, "sha512-07GVQo/38a0yvIPlWDr3RJn1B8gk3ZuIX9h2oIQ+Biyu3JN0KppWmgWHfaWRydQgse5JtC++KDw5MWaIRnV0mw=="],
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@workspace:packages/fff-bun"],
"@ff-labs/fff-node": ["@ff-labs/fff-node@workspace:packages/fff-node"],
"@ff-labs/pi-fff": ["@ff-labs/pi-fff@workspace:packages/pi-fff"],
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
"@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="],
"@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="],
"@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="],
"@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="],
"@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="],
"@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="],
"@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="],
"@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="],
"@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="],
"@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="],
"@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="],
"@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="],
"@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@smithy/core": ["@smithy/core@3.24.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.8", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ=="],
"@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-V4nmlXdOYZEa7GOxSExVG95SLp8FE0iTq2yKeN54UlfNMr3Sik+1Ff57LcCv7qYcn4TBqnBAt5rT3FAM6T6caQ=="],
"@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YlnTMIyzfW3mAULC5ZA774nzQfFlYXM0rrfq/8ZzWt+IMbYk55a++jrI+6JeKV+1EqlDS3TFBEFtjdBNG94KzQ=="],
"@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-sI3LpQQ34SX4nyOHc5yxA7FSqs9qPEUMqW/y/wWo9cuyPpaHMFsi/BeOVYsnC0syp3FrY7gzn6RnD6PlXCktXg=="],
"@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-1WkcGkJTlwh4ZA59htKI+RXhiL3oKiYwLv7PO8LUf6FuADK73s5GcXp67iakKu243uYu+qGYr4RHco4ySddYhQ=="],
"@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-J2PwqviycZxaEVA0Bwv38LqGDGSB9A1DPN4iYginYJZSvTvKW8kh7Tis0HbZrX1YDKnY8hi3lt0N0tCTNPDH5Q=="],
"@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hn1W1hBPssTaqikU1Bqp1XUdDdOgbnYVIOtR++LVx66hhrtjf/xrIUQOhTm+NmOFDG16JUKXe1skfM4gpaqYwg=="],
"@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kW6e+oCYZPvpH2ppPsffA18e1aLowtmWTRjVlyHtY04g/nQDepQvDUkkcvInh9fW5jLna7PjHvktW1tVgYIj2A=="],
"@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HTwblAzruUS16nQPrez3ozvEHm1Xxh8J8w7rZYrpmAcNl1hzyOT8z/hY70M9Rt9fOqQ4Ovgor9qVy/U3ZJo0ZA=="],
"@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-WeZkGl2BP1U4tRhEQH+FXLQS52N8obp74smK5AAGOfzPAT1pHkq6+dVkC1QCSIt7dHJs7SPtlnQw+5DkdZYlWA=="],
"@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.1", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-rNGgMeCH5mdeHiMiJgt7wWXovZ+FHEfXhU9p4zZBH4n8M1/QnEsRUwlapISPLpILSGpoYS6iBuq9/fUlZY8Mhg=="],
"@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dr2LcLD2CXo2a7BktlOpV68QhayqiI112KxIJC9tBgQO/Dkdg4CPsdqmvzzLhFo64iC5RLl2BT7M5lJImrfUWw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="],
"fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"ffi-rs": ["ffi-rs@1.3.1", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.1", "@yuuang/ffi-rs-darwin-arm64": "1.3.1", "@yuuang/ffi-rs-darwin-x64": "1.3.1", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.1", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.1", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.1", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.1", "@yuuang/ffi-rs-linux-x64-musl": "1.3.1", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.1", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.1", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.1" } }, "sha512-ZyNXL9fnclnZV+waQmWB9JrfbIEyxQa1OWtMrHOrAgcC04PgP5hBMG5TdhVN8N4uT/eul8zCFMVnJUukAFFlXA=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"google-auth-library": ["google-auth-library@10.7.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1063.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.18", "@aws-sdk/nested-clients": "^3.997.17", "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-nYDaWWdzjKiDP5xj8k4oUgcYd4WPgzfAOgdU5vJsaqH/07Dfvm7ffisHCFJ+NEl7kUC9JEIUxh0kznvenbo3NQ=="],
"@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A=="],
"bun-types/@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"protobufjs/@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-c"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
description = "Raw C api of FFF file finder"
license = "MIT"
@@ -19,6 +19,6 @@ zlob = ["fff/zlob", "fff-query-parser/zlob"]
[dependencies]
git2.workspace = true
fff = { package = "fff-search", path = "../fff-core", version = "0.10.0", default-features = false }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.0", default-features = false }
fff = { package = "fff-search", path = "../fff-core", version = "0.10.5", default-features = false }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.5", default-features = false }
serde_json = "1.0"
+3 -10
View File
@@ -729,19 +729,12 @@ pub unsafe extern "C" fn fff_multi_grep(
}
};
let is_ai = picker.mode().is_ai();
// Parse constraints from the optional string (e.g. "*.rs /src/")
let parsed_constraints = constraints_str.map(|c| {
if is_ai {
fff::QueryParser::new(fff_query_parser::AiGrepConfig).parse(c)
} else {
fff::grep::parse_grep_query(c)
}
});
let parsed_constraints = constraints_str
.map(|c| fff::QueryParser::new(fff_query_parser::AiGrepConfig).parse_constraints(c));
let constraint_refs: &[fff::Constraint<'_>] = match &parsed_constraints {
Some(q) => &q.constraints,
Some(constraints) => constraints,
None => &[],
};
+5 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-search"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
license = "MIT"
authors = ["Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>"]
@@ -11,7 +11,7 @@ workspace = true
[lib]
path = "src/lib.rs"
crate-type = ["rlib", "staticlib", "cdylib"]
crate-type = ["rlib"]
[[bench]]
name = "parse_bench"
@@ -41,6 +41,9 @@ harness = false
default = ["ripgrep"]
# Enable C FFI exports
ffi = []
# Count full rescans and their causes. Always on in debug builds; enable this
# to keep the accounting in a release build (used by the rescan_probe binary).
rescan-stats = []
# Enables POC definition classification for grep result matched lines
definitions = []
# Pure-Rust filesystem walker + glob matcher (ignore + globset crates).
+2 -8
View File
@@ -1,11 +1,5 @@
//! Compare three glob-matching strategies for `match_glob_pattern` in constraints.rs:
//!
//! 1. Current: `zlob_match_paths` -> collect `as_ptr()` into AHashSet, filter paths
//! by pointer to recover indices.
//! 2. Free fn: `zlob_match_paths_indices` (added in zlob 1.4) — indices direct from C.
//! 3. Compiled: `ZlobPattern::compile` + `match_indices` — same indices path, but with
//! a precompiled pattern (reusable). For one-shot it should match (2); the win
//! appears if the pattern is reused (chunked / repeated calls).
//! Compare three glob-matching strategies for `match_glob_pattern` in constraints.rs
//! need to make sure that my zlob changes are not affecting fff's internal globbing
use ahash::AHashSet;
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use zlob::{ZlobFlags, ZlobPattern, zlob_match_paths, zlob_match_paths_indices};
+9
View File
@@ -3,6 +3,15 @@ fn main() {
// used by tests/fuzz_git_watcher_stress.rs
println!("cargo::rustc-check-cfg=cfg(stress)");
// Full-rescan accounting. Debug builds get it for free; a release build has
// to opt in with `--features rescan-stats` (what the rescan_probe needs).
println!("cargo::rustc-check-cfg=cfg(rescan_stats)");
if std::env::var("DEBUG").is_ok_and(|debug| debug != "false")
|| std::env::var("CARGO_FEATURE_RESCAN_STATS").is_ok()
{
println!("cargo::rustc-cfg=rescan_stats");
}
// When the `zlob` feature is enabled (Zig-compiled C library):
// On Windows MSVC, explicitly link the C runtime libraries.
// Zig-compiled static libraries don't emit /DEFAULTLIB directives for the
+12 -3
View File
@@ -12,16 +12,25 @@ pub const MAX_INDEXABLE_FILE_SIZE: usize = 2 * 1024 * 1024;
pub const MAX_CACHED_CONTENT_BYTES: u64 = 512 * 1024 * 1024;
/// Files below one page waste the remainder when mmapped, so the cache skips
/// them and falls back to chunked reads. Unused on Windows (no content cache).
/// them and falls back to chunked reads. Unused on Windows (no content cache)
#[cfg(all(not(target_os = "windows"), target_arch = "aarch64"))]
pub const MMAP_THRESHOLD: u64 = 16 * 1024;
#[cfg(all(not(target_os = "windows"), not(target_arch = "aarch64")))]
pub const MMAP_THRESHOLD: u64 = 4 * 1024;
/// Capacity reserved for files the watcher discovers after the initial scan;
/// exceeding it forces a full rescan.
/// Watcher overflow capacity reserved after the initial scan
pub const MAX_OVERFLOW_FILES: usize = 1024;
/// Minimum delay between watcher-initiated rescans.
pub const RESCAN_MIN_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
/// Rescan delay for large indexes.
pub const RESCAN_MIN_INTERVAL_LARGE_INDEX: std::time::Duration =
std::time::Duration::from_secs(5 * 60);
/// Live-file count at which [`RESCAN_MIN_INTERVAL_LARGE_INDEX`] takes over.
pub const LARGE_INDEX_FILE_COUNT: usize = 1_000_000;
/// Fresh-mmap threshold: files at or above this size get mmapped directly on
/// cache miss instead of chunked reads into Vec. Empirically tuned per-platform.
/// Only referenced on Unix; Windows uses the `std::fs::read` fallback so this
+1 -1
View File
@@ -14,7 +14,7 @@ pub struct DbHealth {
}
pub trait DbHealthChecker {
fn get_env(&self) -> &heed::Env;
fn get_env(&self) -> &heed::Env<heed::WithoutTls>;
fn is_healthy(&self) -> bool;
/// Entries per database, each group has a static string label
fn count_entries(&self) -> Result<Vec<(&'static str, u64)>>;
+284
View File
@@ -0,0 +1,284 @@
use heed::{Env, EnvOpenOptions, WithoutTls};
use std::collections::HashMap;
use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError, Weak};
use std::thread;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::lmdb::DbHealth;
pub(crate) struct EnvSpec {
pub label: &'static str,
pub map_size: usize,
pub max_dbs: u32,
pub size_cap_bytes: u64,
}
pub(crate) struct PooledEnv {
env: Env<WithoutTls>,
key: PathBuf,
/// lmdb's env spec label
label: &'static str,
map_size: usize,
max_dbs: u32,
health: DbHealth,
gc_started: AtomicBool,
dbi_lock: Mutex<()>,
}
impl Drop for PooledEnv {
fn drop(&mut self) {
let mut pool = POOL.lock().unwrap_or_else(PoisonError::into_inner);
// Only remove a dead entry: begin_exclusive_destroy may have removed ours.
if pool.get(&self.key).is_some_and(|w| w.strong_count() == 0) {
pool.remove(&self.key);
}
// heed closes the env right after this body; a concurrent reopen of the
// same path rides out that gap via env_closing_event in get_or_open.
}
}
// Cloneable handle to a process-shared LMDB env, derefs to `heed::Env`.
#[derive(Clone)]
pub(crate) struct SharedEnv(Arc<PooledEnv>);
impl Deref for SharedEnv {
type Target = Env<WithoutTls>;
fn deref(&self) -> &Env<WithoutTls> {
&self.0.env
}
}
impl std::fmt::Debug for SharedEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SharedEnv").field(&self.0.env).finish()
}
}
impl SharedEnv {
pub(crate) fn get_or_open(db_path: &Path, spec: &EnvSpec) -> Result<Self> {
fs::create_dir_all(db_path).map_err(Error::CreateDir)?;
let path = fs::canonicalize(db_path).map_err(|e| Error::EnvOpen {
db: spec.label,
source: heed::Error::Io(e),
})?;
let mut close_waits = 0u32;
let mut transient_retries = 0u32;
loop {
let mut open_failed = false;
{
let mut pool = POOL.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(existing) = pool.get(&path).and_then(Weak::upgrade) {
drop(pool);
if existing.label != spec.label
|| existing.map_size != spec.map_size
|| existing.max_dbs != spec.max_dbs
{
return Err(Error::EnvSpecMismatch {
path,
open_as: existing.label,
requested_as: spec.label,
});
}
return Ok(Self(existing));
}
erase_if_oversized(&path, spec);
let result = unsafe {
// MDB_NOTLS: reader slots are tied to txn objects (freed on
// commit/abort) instead of pinned per thread for its lifetime (#783).
let mut opts = EnvOpenOptions::new().read_txn_without_tls();
opts.map_size(spec.map_size);
opts.max_readers(max_readers());
if spec.max_dbs > 0 {
opts.max_dbs(spec.max_dbs);
}
opts.open(&path)
};
match result {
Ok(env) => {
let entry = Arc::new(PooledEnv {
env,
key: path.clone(),
label: spec.label,
map_size: spec.map_size,
max_dbs: spec.max_dbs,
health: DbHealth::new(),
gc_started: AtomicBool::new(false),
dbi_lock: Mutex::new(()),
});
pool.insert(path.clone(), Arc::downgrade(&entry));
drop(pool);
let shared = Self(entry);
match shared.clear_stale_readers() {
Ok(cleared_count) if cleared_count > 0 => {
tracing::info!(
cleared_count,
db = spec.label,
"reclaimed stale LMDB reader slots at open"
);
}
Ok(_) => {}
Err(e) => {
tracing::debug!("clear_stale_readers at open failed: {e}")
}
}
return Ok(shared);
}
Err(heed::Error::EnvAlreadyOpened) => open_failed = true,
// special handling cause we know this happens randomly
Err(e)
if is_transient_env_open_error(&e)
&& transient_retries < MAX_TRANSIENT_RETRIES =>
{
transient_retries += 1;
tracing::debug!(
path = %path.display(),
transient_retries,
error = ?e,
"transient LMDB env open error, retrying"
);
}
Err(e) => {
return Err(Error::EnvOpen {
db: spec.label,
source: e,
});
}
}
}
if open_failed {
close_waits += 1;
if close_waits > MAX_CLOSE_WAITS {
return Err(Error::EnvOpen {
db: spec.label,
source: heed::Error::EnvAlreadyOpened,
});
}
match heed::env_closing_event(&path) {
Some(event) => {
event.wait_timeout(CLOSE_WAIT);
}
None => thread::sleep(Duration::from_millis(2)),
}
} else {
thread::sleep(TRANSIENT_RETRY_SLEEP);
}
}
}
pub(crate) fn health(&self) -> &DbHealth {
&self.0.health
}
// First caller wins: GC runs once per opened env, not once per tracker.
pub(crate) fn try_start_gc(&self) -> bool {
!self.0.gc_started.swap(true, Ordering::AcqRel)
}
// LMDB forbids mdb_dbi_open from concurrent txns in the same process.
pub(crate) fn lock_dbi_open(&self) -> MutexGuard<'_, ()> {
self.0
.dbi_lock
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn destroy(&self) -> Result<Option<heed::EnvClosingEvent>> {
let mut pool = POOL.lock().unwrap_or_else(PoisonError::into_inner);
let holders = Arc::strong_count(&self.0);
if holders > 1 {
return Err(Error::DbInUse {
db: self.0.label,
path: self.0.key.clone(),
holders: holders - 1,
});
}
pool.remove(&self.0.key);
Ok(heed::env_closing_event(&self.0.key))
}
}
static POOL: LazyLock<Mutex<HashMap<PathBuf, Weak<PooledEnv>>>> = LazyLock::new(Mutex::default);
const CLOSE_WAIT: Duration = Duration::from_millis(100);
const MAX_CLOSE_WAITS: u32 = 100;
const TRANSIENT_RETRY_SLEEP: Duration = Duration::from_millis(50);
const MAX_TRANSIENT_RETRIES: u32 = 8;
// Concurrent mdb_env_open calls on the same path can race on macOS
// this is for some reason fixable by simple retry of the open
// heed's default reader table is 126 slots. In TLS mode each thread pins a slot
// for its lifetime, so long-lived embedders (Neovim, node agents) that share one
// lock file across many processes/threads exhaust it (#783). Reader slots are
// tiny (~64B), so raise the ceiling; `FFF_LMDB_MAX_READERS` lets hosts tune it.
const DEFAULT_MAX_READERS: u32 = 1024;
fn max_readers() -> u32 {
parse_max_readers(std::env::var("FFF_LMDB_MAX_READERS").ok())
}
// Never drop below heed's default 126; ignore missing/garbage/too-small values.
fn parse_max_readers(raw: Option<String>) -> u32 {
raw.and_then(|v| v.trim().parse::<u32>().ok())
.filter(|&n| n >= 126)
.unwrap_or(DEFAULT_MAX_READERS)
}
fn is_transient_env_open_error(err: &heed::Error) -> bool {
match err {
heed::Error::Io(io) => matches!(
io.kind(),
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::NotFound
),
_ => false,
}
}
fn erase_if_oversized(db_path: &Path, spec: &EnvSpec) {
let data = db_path.join("data.mdb");
let Ok(meta) = fs::metadata(&data) else {
return;
};
if meta.len() <= spec.size_cap_bytes {
return;
}
tracing::error!(
path = %db_path.display(),
size = meta.len(),
cap = spec.size_cap_bytes,
"LMDB db exceeds size cap, erasing"
);
let _ = fs::remove_file(&data);
let _ = fs::remove_file(db_path.join("lock.mdb"));
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_MAX_READERS, parse_max_readers};
#[test]
fn max_readers_parsing() {
assert_eq!(parse_max_readers(None), DEFAULT_MAX_READERS);
assert_eq!(parse_max_readers(Some("nan".into())), DEFAULT_MAX_READERS);
assert_eq!(parse_max_readers(Some("64".into())), DEFAULT_MAX_READERS); // below 126 floor
assert_eq!(parse_max_readers(Some(" 512 ".into())), 512);
assert_eq!(parse_max_readers(Some("126".into())), 126);
}
}
+8 -7
View File
@@ -1,10 +1,11 @@
use super::db_healthcheck::DbHealthChecker;
use super::lmdb::{DbHealth, LmdbStore, is_map_full};
use super::env_pool::SharedEnv;
use crate::error::{Error, Result};
use crate::file_picker::FFFMode;
use crate::git::is_modified_status;
use crate::lmdb::{DbHealth, LmdbStore, is_map_full};
use heed::Database;
use heed::types::{Bytes, SerdeBincode};
use heed::{Database, Env};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{collections::VecDeque, path::Path};
@@ -19,7 +20,7 @@ const AI_MAX_HISTORY_DAYS: f64 = 7.0; // Only consider accesses within 7 days
#[derive(Debug)]
pub struct FrecencyTracker {
env: Env,
env: SharedEnv,
db: Database<Bytes, SerdeBincode<VecDeque<u64>>>,
health: DbHealth,
}
@@ -42,7 +43,7 @@ const AI_MODIFICATION_THRESHOLDS: [(i64, u64); 5] = [
];
impl DbHealthChecker for FrecencyTracker {
fn get_env(&self) -> &heed::Env {
fn get_env(&self) -> &heed::Env<heed::WithoutTls> {
&self.env
}
@@ -77,7 +78,7 @@ impl LmdbStore for FrecencyTracker {
// MAP_SIZE so we don't hit MDB_MAP_FULL before the open-time erase fires.
const SIZE_CAP_BYTES: u64 = 12 * 1024 * 1024;
fn env(&self) -> &Env {
fn shared_env(&self) -> &SharedEnv {
&self.env
}
@@ -85,7 +86,7 @@ impl LmdbStore for FrecencyTracker {
&self.health
}
fn purge_stale_data(env: &Env) -> Result<()> {
fn purge_stale_data(env: &SharedEnv) -> Result<()> {
let (deleted, pruned) = Self::purge_stale_entries(env)?;
if deleted > 0 || pruned > 0 {
tracing::info!(deleted, pruned, "Frecency GC purged entries");
@@ -121,7 +122,7 @@ impl FrecencyTracker {
/// Removes entries where all timestamps are older than MAX_HISTORY_DAYS,
/// and prunes stale timestamps from entries that still have recent ones.
/// Returns (deleted_count, pruned_count).
fn purge_stale_entries(env: &Env) -> Result<(usize, usize)> {
fn purge_stale_entries(env: &SharedEnv) -> Result<(usize, usize)> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
+34 -96
View File
@@ -1,12 +1,11 @@
use heed::{Database, Env, EnvOpenOptions};
use std::fs;
use heed::{Database, Env, WithoutTls};
use std::path::Path;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU8, Ordering};
use std::thread;
use std::time::Duration;
use super::env_pool::{EnvSpec, SharedEnv};
use crate::error::{Error, Result};
pub(crate) fn is_map_full(err: &heed::Error) -> bool {
@@ -85,9 +84,13 @@ pub(crate) fn spawn_lmdb_gc<T: LmdbStore>(shared: Arc<RwLock<Option<T>>>) {
let Some(ref tracker) = *guard else {
return; // destroyed before we started
};
let env = tracker.env();
// Trackers attaching to an already-pooled env must not repeat the
// GC; the first opener's run flips the shared health flag.
if !tracker.shared_env().try_start_gc() {
return;
}
if let Err(e) = T::purge_stale_data(env) {
if let Err(e) = T::purge_stale_data(tracker.shared_env()) {
tracing::debug!("purge_stale_data failed: {e}");
}
@@ -105,18 +108,6 @@ pub(crate) fn spawn_lmdb_gc<T: LmdbStore>(shared: Arc<RwLock<Option<T>>>) {
}
}
// Concurrent `mdb_env_open` calls on the same path can race on macOS
// this is for some reason fixabtly by simple retry of the open
fn is_transient_env_open_error(err: &heed::Error) -> bool {
match err {
heed::Error::Io(io) => matches!(
io.kind(),
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::NotFound
),
_ => false,
}
}
pub(crate) trait LmdbStore: Sized + Send + Sync + 'static {
/// Short label used to defferintiate different instances of this trait
const LABEL: &'static str;
@@ -127,85 +118,51 @@ pub(crate) trait LmdbStore: Sized + Send + Sync + 'static {
/// Hard cap on `data.mdb` size.
const SIZE_CAP_BYTES: u64;
/// Borrow the env in the read lock
fn env(&self) -> &Env;
/// Borrow the pooled env handle shared by every tracker of this path.
fn shared_env(&self) -> &SharedEnv;
/// Borrow the health flag from the tracker.
fn health(&self) -> &DbHealth;
/// Borrow the raw heed env.
fn env(&self) -> &Env<WithoutTls> {
self.shared_env()
}
/// Override to purge stale rows, compact, etc. Default no-op. Runs on
/// the GC thread while a read lock is held against the shared handle,
/// so destroy / re-init naturally wait for it.
fn purge_stale_data(_env: &Env) -> Result<()> {
fn purge_stale_data(_env: &SharedEnv) -> Result<()> {
Ok(())
}
/// Open the LMDB env. Returns env + a `DbHealth` starting in Pending;
/// the GC thread spawned by `spawn_gc` flips it to Healthy. Write
/// paths flip it to Degraded on MDB_MAP_FULL.
/// Open (or join) the process-shared LMDB env for `db_path`. The health
/// flag is per-env: the GC of the first opener flips it for everyone.
#[tracing::instrument]
fn open_env(db_path: &Path) -> Result<(Env, DbHealth)> {
Self::erase_if_oversized(db_path);
fs::create_dir_all(db_path).map_err(Error::CreateDir)?;
let db = Self::LABEL;
const MAX_ATTEMPTS: u32 = 8;
let mut attempt = 0u32;
let env = loop {
let result = unsafe {
let mut opts = EnvOpenOptions::new();
opts.map_size(Self::MAP_SIZE);
if Self::MAX_DBS > 0 {
opts.max_dbs(Self::MAX_DBS);
}
opts.open(db_path)
};
match result {
Ok(env) => break env,
Err(e) if is_transient_env_open_error(&e) && attempt + 1 < MAX_ATTEMPTS => {
attempt += 1;
tracing::debug!(
path = %db_path.display(),
attempt,
error = ?e,
"transient LMDB env open error, retrying"
);
thread::sleep(Duration::from_millis(50));
}
Err(e) => return Err(Error::EnvOpen { db, source: e }),
}
};
// Reclaim reader slots left behind by prior processes that died
// without cleanup. Must run before we start any read txns (which
// open_database_safe does) — otherwise we may hit MDB_READERS_FULL
// on a fresh env just because lock.mdb still has stale entries
// from a previous crash.
//
// This is the one LMDB maintenance call we run on the caller's
// thread. If the lock file is genuinely wedged this will block
// forever, but the alternative — never getting past init — is
// worse and the bg-thread trick doesn't solve it anyway.
match env.clear_stale_readers() {
Ok(cleared) if cleared > 0 => {
tracing::warn!(cleared, "reclaimed stale LMDB reader slots at open");
}
Ok(_) => {}
Err(e) => tracing::debug!("clear_stale_readers at open failed: {e}"),
}
Ok((env, DbHealth::new()))
fn open_env(db_path: &Path) -> Result<(SharedEnv, DbHealth)> {
let shared = SharedEnv::get_or_open(
db_path,
&EnvSpec {
label: Self::LABEL,
map_size: Self::MAP_SIZE,
max_dbs: Self::MAX_DBS,
size_cap_bytes: Self::SIZE_CAP_BYTES,
},
)?;
let health = shared.health().clone();
Ok((shared, health))
}
/// Open or create a database without blocking on the LMDB writer mutex
/// when the database already exists.
fn open_database_safe<KC, DC>(env: &Env, name: Option<&str>) -> Result<Database<KC, DC>>
fn open_database_safe<KC, DC>(env: &SharedEnv, name: Option<&str>) -> Result<Database<KC, DC>>
where
KC: 'static,
DC: 'static,
{
let db = Self::LABEL;
// mdb_dbi_open must not run from concurrent txns in this process.
let _dbi_guard = env.lock_dbi_open();
let rtxn = env
.read_txn()
.map_err(|source| Error::DbStartReadTxn { db, source })?;
@@ -237,23 +194,4 @@ pub(crate) trait LmdbStore: Sized + Send + Sync + 'static {
}
}
}
fn erase_if_oversized(db_path: &Path) {
let data = db_path.join("data.mdb");
let Ok(meta) = fs::metadata(&data) else {
return;
};
if meta.len() <= Self::SIZE_CAP_BYTES {
return;
}
tracing::error!(
path = %db_path.display(),
size = meta.len(),
cap = Self::SIZE_CAP_BYTES,
"LMDB db exceeds size cap, erasing"
);
let _ = fs::remove_file(&data);
let _ = fs::remove_file(db_path.join("lock.mdb"));
}
}
+1
View File
@@ -1,3 +1,4 @@
pub(crate) mod env_pool;
pub(crate) mod lmdb;
pub mod db_healthcheck;
+6 -5
View File
@@ -1,6 +1,7 @@
use super::db_healthcheck::DbHealthChecker;
use super::lmdb::{DbHealth, LmdbStore, is_map_full};
use super::env_pool::SharedEnv;
use crate::error::Error;
use crate::lmdb::{DbHealth, LmdbStore, is_map_full};
use heed::types::{Bytes, SerdeBincode};
use heed::{Database, Env};
use serde::{Deserialize, Serialize};
@@ -27,7 +28,7 @@ struct HistoryEntry {
#[derive(Debug)]
pub struct QueryTracker {
env: Env,
env: SharedEnv,
// Database for (project_path, query) -> QueryMatchEntry mappings
query_file_db: Database<Bytes, SerdeBincode<QueryMatchEntry>>,
// Database for project_path -> VecDeque<HistoryEntry> mappings (file picker)
@@ -38,7 +39,7 @@ pub struct QueryTracker {
}
impl DbHealthChecker for QueryTracker {
fn get_env(&self) -> &Env {
fn get_env(&self) -> &Env<heed::WithoutTls> {
&self.env
}
@@ -92,7 +93,7 @@ impl LmdbStore for QueryTracker {
const MAX_DBS: u32 = 16;
const SIZE_CAP_BYTES: u64 = 8 * 1024 * 1024;
fn env(&self) -> &Env {
fn shared_env(&self) -> &SharedEnv {
&self.env
}
@@ -197,7 +198,7 @@ impl QueryTracker {
/// offset=0 returns most recent, offset=1 returns 2nd most recent, etc.
fn read_history_at_offset(
db: &Database<Bytes, SerdeBincode<VecDeque<HistoryEntry>>>,
env: &Env,
env: &Env<heed::WithoutTls>,
project_key: &[u8; 32],
offset: usize,
) -> Result<Option<String>, Error> {
+16
View File
@@ -34,6 +34,22 @@ pub enum Error {
#[source]
source: heed::Error,
},
#[error(
"LMDB env at {path} is already open as the '{open_as}' database with different options; requested by '{requested_as}'. Use a distinct path per database."
)]
EnvSpecMismatch {
path: std::path::PathBuf,
open_as: &'static str,
requested_as: &'static str,
},
#[error(
"The {db} database at {path} is still used by {holders} other tracker(s) in this process"
)]
DbInUse {
db: &'static str,
path: std::path::PathBuf,
holders: usize,
},
#[error("Failed to create {db} database: {source}")]
DbCreate {
db: &'static str,
+180 -49
View File
@@ -45,6 +45,7 @@ use crate::types::{
ContentCacheBudget, DirItem, DirSearchResult, FileItem, MixedItemRef, MixedSearchResult,
PaginationArgs, Score, ScoringContext, SearchResult,
};
use crate::walk::WalkOutput;
use crate::watch::BackgroundWatcher;
use fff_query_parser::FFFQuery;
use git2::{Repository, Status};
@@ -628,6 +629,10 @@ impl FilePicker {
&self.base_path
}
pub fn has_git_repo(&self) -> bool {
self.sync_data.git_workdir.is_some()
}
/// Ignore rules the walker assembled during the last scan (zlob backend
/// only). The background watcher uses these to filter events without
/// libgit2. `None` when the backend doesn't surface rules or no ignore
@@ -2020,30 +2025,38 @@ impl FileSync {
let is_git_repo = git_workdir.is_some();
let bg_threads = BACKGROUND_THREAD_POOL.current_num_threads();
let mut walk_output = crate::walk::walk_collect_files(
let WalkOutput {
dirs: mut walked_dirs,
mut pairs,
ignore_rules,
} = crate::walk::walk_collect_files(
base_path,
is_git_repo,
follow_symlinks,
bg_threads,
synced_files_count,
)?;
let ignore_rules = walk_output.ignore_rules.take().map(Arc::new);
let mut pairs = walk_output.pairs;
let ignore_rules = ignore_rules.map(Arc::new);
// Sort by (dir_part, filename). This groups files by their directory
// into contiguous runs so the linear dir-extraction pass below can
// dedupe by comparing only against the previous dir.
// group walked dirs and files with a dir part to the same order
BACKGROUND_THREAD_POOL.install(|| {
pairs.par_sort_unstable_by(|(a, path_a), (b, path_b)| {
// SAFETY: `filename_offset` is always at a character boundary
let (a_dir, a_file) = path_a.split_at(a.path.filename_offset as usize);
let (b_dir, b_file) = path_b.split_at(b.path.filename_offset as usize);
a_dir.cmp(b_dir).then_with(|| a_file.cmp(b_file))
});
rayon::join(
|| {
pairs.par_sort_unstable_by(|(a, path_a), (b, path_b)| {
// SAFETY: `filename_offset` is always at a character boundary
let (a_dir, a_file) = path_a.split_at(a.path.filename_offset as usize);
let (b_dir, b_file) = path_b.split_at(b.path.filename_offset as usize);
a_dir.cmp(b_dir).then_with(|| a_file.cmp(b_file))
});
},
|| walked_dirs.par_sort_unstable(),
);
});
walked_dirs.dedup();
let mut builder = crate::simd_path::ChunkedPathStoreBuilder::new(pairs.len());
let dirs = populates_dirs_files_chunked_storage(&mut pairs, &mut builder);
let dirs = populates_dirs_files_chunked_storage(&mut pairs, &walked_dirs, &mut builder);
drop(walked_dirs);
let mut files: Vec<FileItem> = pairs.into_iter().map(|(file, _)| file).collect();
let chunked_paths = builder.finish();
@@ -2164,49 +2177,91 @@ pub(crate) fn warmup_mmaps(
}
/// This does both thing (yes sorry all the OOP morons)
/// in one go: populates files chunked storage and creates new directories
/// in one go: populates files chunked storage and builds the dir table from
/// `walked_dirs` (every dir the walker visited: sorted, '/'-terminated,
/// deduped), merging file parents in a single lockstep sweep so dirs with no
/// files (empty subtrees, pure ancestors) are indexed and searchable too.
fn populates_dirs_files_chunked_storage<'a>(
pairs: &'a mut [(FileItem, String)],
walked_dirs: &[String],
chunk_storage: &mut crate::simd_path::ChunkedPathStoreBuilder,
) -> Vec<DirItem> {
let mut dirs: Vec<DirItem> = Vec::new();
let mut dirs: Vec<DirItem> = Vec::with_capacity(walked_dirs.len() + 1);
let mut dir_iter = walked_dirs.iter().peekable();
// Root-level files sort first and their "" parent is never a walker dir.
if pairs
.first()
.is_some_and(|(f, _)| f.path.filename_offset == 0)
{
push_dir_item(&mut dirs, chunk_storage, "");
}
// Detects contiguous same-dir runs (pairs are sorted by dir) so the
// merge below runs once per directory, not once per file.
let mut prev_dir: &'a str = "";
let mut prev_dir_valid = false;
let mut current_dir_idx: u32 = 0;
for (file, rel) in pairs.iter_mut() {
let rel: &'a str = rel;
let dir_part: &'a str = &rel[..file.path.filename_offset as usize];
if !prev_dir_valid || prev_dir != dir_part {
let dir_string = chunk_storage.add_dir_immediate(dir_part);
if prev_dir != dir_part {
// Flush walked dirs up to and including this file's parent,
// keeping the table sorted for the find_dir_index binary search.
while let Some(dir) = dir_iter.peek()
&& dir.as_str() < dir_part
{
push_dir_item(&mut dirs, chunk_storage, dir);
dir_iter.next();
}
// Compute last-segment offset: for "src/components/" -> 4 (points to "components/")
let last_seg = if dir_part.is_empty() {
0
} else {
let trimmed = dir_part.trim_end_matches(std::path::is_separator);
trimmed
.rfind(std::path::is_separator)
.map(|i| i + 1)
.unwrap_or(0) as u16
};
match dir_iter.peek() {
Some(dir) if dir.as_str() == dir_part => {
push_dir_item(&mut dirs, chunk_storage, dir);
dir_iter.next();
}
// Parents the walker reported with a non-dir kind
// (e.g. followed symlinks) aren't in the list.
_ => push_dir_item(&mut dirs, chunk_storage, dir_part),
}
dirs.push(DirItem::new(dir_string, last_seg));
current_dir_idx = (dirs.len() - 1) as u32;
prev_dir = dir_part;
prev_dir_valid = true;
}
file.path = chunk_storage.add_file_immediate(rel, file.path.filename_offset);
file.parent_dir_index = current_dir_idx;
}
for dir in dir_iter {
push_dir_item(&mut dirs, chunk_storage, dir);
}
dirs
}
fn push_dir_item(
dirs: &mut Vec<DirItem>,
chunk_storage: &mut crate::simd_path::ChunkedPathStoreBuilder,
dir_part: &str,
) {
let dir_string = chunk_storage.add_dir_immediate(dir_part);
// Compute last-segment offset: for "src/components/" -> 4 (points to "components/")
let last_seg = if dir_part.is_empty() {
0
} else {
let trimmed = dir_part.trim_end_matches(std::path::is_separator);
trimmed
.rfind(std::path::is_separator)
.map(|i| i + 1)
.unwrap_or(0) as u16
};
dirs.push(DirItem::new(dir_string, last_seg));
}
/// Fast extension-based binary detection. Avoids opening files during scan.
/// Covers the vast majority of binary files in typical repositories.
#[inline]
@@ -2341,13 +2396,9 @@ mod tests {
use super::*;
/// The watcher must watch every ancestor directory up to `base_path`,
/// not just the immediate parents of indexed files. Intermediate dirs
/// that contain only subdirectories (no direct files) are NOT in
/// `sync_data.dirs` — yet they must still appear in `extract_watch_dirs`
/// so Create events on new subdirectories below them fire.
///
/// Correctness regression guard for any refactor that replaces the
/// ancestor walk with a direct `sync_data.dirs` iteration.
/// not just the immediate parents of indexed files. The dir table is
/// built from the walker's visited dirs, so pure ancestors (dirs that
/// contain only subdirectories) must be present and emitted exactly once.
#[test]
fn extract_watch_dirs_includes_pure_ancestor_dirs() {
let dir = tempfile::tempdir().unwrap();
@@ -2361,17 +2412,6 @@ mod tests {
// base/src/components/button.txt (src/components has a file)
// base/src/routes/home.txt (src/routes has a file)
// base/lib/deep/nested/util.txt (lib and lib/deep have no files)
//
// `sync_data.dirs` will only contain:
// src/components/
// src/routes/
// lib/deep/nested/
//
// But the watcher also needs:
// src/ (pure ancestor — no direct files)
// lib/ (pure ancestor)
// lib/deep/ (pure ancestor)
// otherwise new siblings like `src/NewDir/x.txt` are missed.
for rel in [
"src/components/button.txt",
"src/routes/home.txt",
@@ -2429,6 +2469,97 @@ mod tests {
);
}
/// Regression guard for #725: dirs that are EMPTY at scan time are merged
/// into `sync_data.dirs` so they are searchable and get an inotify watch;
/// files created in them later must be detected.
#[test]
fn for_each_dir_includes_empty_directories() {
let dir = tempfile::tempdir().unwrap();
let base_buf = crate::path_utils::canonicalize(dir.path()).unwrap();
let base = base_buf.as_path();
// Tree:
// base/init.lua (file directly under base)
// base/commands/ (empty at scan — the #725 repro)
// base/src/main.rs (src is indexed)
// base/src/plugins/extra/ (empty chain under an indexed dir)
std::fs::create_dir_all(base.join("commands")).unwrap();
std::fs::create_dir_all(base.join("src/plugins/extra")).unwrap();
std::fs::write(base.join("init.lua"), b"x").unwrap();
std::fs::write(base.join("src/main.rs"), b"x").unwrap();
let mut picker = FilePicker::new(FilePickerOptions {
base_path: base.to_str().unwrap().into(),
watch: false,
..Default::default()
})
.unwrap();
picker.collect_files().unwrap();
let mut watch_dirs: Vec<PathBuf> = Vec::new();
picker.for_each_dir(|p| {
watch_dirs.push(p.to_path_buf());
std::ops::ControlFlow::Continue(())
});
let watch_set: std::collections::HashSet<PathBuf> = watch_dirs.iter().cloned().collect();
for rel in ["commands", "src/plugins", "src/plugins/extra", "src"] {
assert!(
watch_set.contains(&base.join(rel)),
"expected {rel} in watch dirs, got {watch_set:?}",
);
}
// Dirs covered by indexed files must not be duplicated.
assert_eq!(
watch_dirs.len(),
watch_set.len(),
"duplicate watch dir emitted: {watch_dirs:?}",
);
}
#[test]
fn dir_table_merges_walked_dirs_with_file_parents() {
let mut pairs: Vec<(FileItem, String)> = ["src/main.rs", "src/deep/lib.rs", "root.txt"]
.iter()
.map(|p| {
let (item, rel) = FileItem::new(PathBuf::from(p), Path::new(""), None);
(item, rel)
})
.collect();
pairs.sort_by(|(a, pa), (b, pb)| {
pa[..a.path.filename_offset as usize]
.cmp(&pb[..b.path.filename_offset as usize])
.then_with(|| pa.cmp(pb))
});
// Sorted '/'-terminated walker output: file parents + an empty dir +
// a sibling sharing a prefix with a file parent.
let walked: Vec<String> = ["empty/", "src/", "src/deep/", "src/deeper/"]
.iter()
.map(|s| s.to_string())
.collect();
let mut builder = crate::simd_path::ChunkedPathStoreBuilder::new(pairs.len());
let dirs = populates_dirs_files_chunked_storage(&mut pairs, &walked, &mut builder);
let store = builder.finish();
let arena = store.as_arena_ptr();
let table: Vec<String> = dirs.iter().map(|d| d.relative_path(arena)).collect();
// Sorted: "" (root files) first, all walked dirs present exactly once.
assert_eq!(table, ["", "empty/", "src/", "src/deep/", "src/deeper/"]);
// Every file's parent_dir_index points at its own dir entry.
for (file, _) in &pairs {
let dir = &dirs[file.parent_dir_index as usize];
let rel = file.relative_path(arena);
assert!(
rel.starts_with(&dir.relative_path(arena)),
"file {rel} must live under its parent dir",
);
}
}
#[test]
fn common_dir_prefix_len_cases() {
assert_eq!(common_dir_prefix_len("", ""), 0);
+1 -1
View File
@@ -62,7 +62,7 @@ impl GitStatusCache {
let mut entries = AHashMap::with_capacity(statuses.len());
for entry in &statuses {
if let Some(entry_path) = entry.path() {
if let Ok(entry_path) = entry.path() {
// libgit2 returns entry paths with forward slashes on every platform
// fff stores native paths - meaning we have forward slash issue on windows
let full_path = crate::path_utils::normalize(repo_path.join(entry_path));
+80
View File
@@ -200,6 +200,85 @@ pub(crate) fn grep_search<'a>(
base_path: &Path,
arena: crate::simd_path::ArenaPtr,
overflow_arena: crate::simd_path::ArenaPtr,
) -> GrepResult<'a> {
let result = grep_search_parsed(
files,
query,
options,
budget,
bigram_index,
bigram_overlay,
abort_signal,
base_path,
arena,
overflow_arena,
);
// Constraint parsing can swallow tokens the user meant literally (e.g. `!=`
// becoming an exclusion). If the constrained search scanned everything and
// found nothing, retry the whole raw query as literal text. This also holds
// for later pages: an empty full scan at offset 0 stays empty at any offset,
// so paging offsets consistently index the literal search's file list.
let full_scan_empty = result.matches.is_empty() && result.next_file_offset == 0;
if !full_scan_empty || query.constraints.is_empty() || abort_signal.load(Ordering::Relaxed) {
return result;
}
let raw = query.raw_query.trim();
if raw.is_empty() {
return result;
}
// Keep any explicit FilePath scope (AI mode `path/to/file.ext` prefix) so the
// fallback can't leak matches outside the file the user pinned. Only the
// swallowed operator/glob tokens are dropped. See issue #756.
let scoped_constraints: fff_query_parser::ConstraintVec<'_> = query
.constraints
.iter()
.filter(|c| matches!(c, fff_query_parser::Constraint::FilePath(_)))
.cloned()
.collect();
let literal_query = FFFQuery {
raw_query: query.raw_query,
constraints: scoped_constraints,
fuzzy_query: fff_query_parser::FuzzyQuery::Text(raw),
location: None,
};
let mut fallback = grep_search_parsed(
files,
&literal_query,
options,
budget,
bigram_index,
bigram_overlay,
abort_signal,
base_path,
arena,
overflow_arena,
);
if fallback.matches.is_empty() {
result
} else {
fallback.literal_fallback = true;
fallback
}
}
#[allow(clippy::too_many_arguments)]
fn grep_search_parsed<'a>(
files: &'a [FileItem],
query: &FFFQuery<'_>,
options: &GrepSearchOptions,
budget: &ContentCacheBudget,
bigram_index: Option<&BigramFilter>,
bigram_overlay: Option<&BigramOverlay>,
abort_signal: &AtomicBool,
base_path: &Path,
arena: crate::simd_path::ArenaPtr,
overflow_arena: crate::simd_path::ArenaPtr,
) -> GrepResult<'a> {
let total_files = files.live_count();
let constraints_from_query = &query.constraints[..];
@@ -622,5 +701,6 @@ where
filtered_file_count: ctx.filtered_file_count,
next_file_offset,
regex_fallback_error: None,
literal_fallback: false,
}
}
+55
View File
@@ -461,3 +461,58 @@ fn test_grep_no_duplicates_with_overflow_trailing_bits() {
result.matches.len()
);
}
/// Issue #756: an AI-mode regex query with an inline FilePath scope and
/// top-level alternation. The regex fragments are swallowed as bogus Glob
/// constraints, the constrained search finds nothing, and the literal/regex
/// fallback must NOT drop the FilePath scope — otherwise the `|` branch leaks
/// matches into files outside the pinned path.
#[test]
fn regex_fallback_keeps_file_path_scope_issue_756() {
use fff_query_parser::{AiGrepConfig, QueryParser};
let dir = tempfile::tempdir().unwrap();
let base = crate::path_utils::canonicalize(dir.path()).unwrap();
std::fs::create_dir(base.join("scope")).unwrap();
std::fs::write(
base.join("scope").join("target.css"),
"/* ---------- target ---------- */\n",
)
.unwrap();
std::fs::write(
base.join("outside.css"),
"/* ---------- outside ---------- */\n",
)
.unwrap();
let mut picker = FilePicker::new(FilePickerOptions {
base_path: base.to_str().unwrap().into(),
watch: false,
..Default::default()
})
.unwrap();
picker.collect_files().unwrap();
let options = crate::GrepSearchOptions {
mode: super::GrepMode::Regex,
smart_case: true,
max_matches_per_file: 80,
page_limit: 100,
..Default::default()
};
let raw = r"scope/target.css ^/\* |^\s*/\* ----------";
let query = QueryParser::new(AiGrepConfig).parse(raw);
let result = picker.grep(&query, &options);
let mut paths: Vec<String> = result
.files
.iter()
.map(|f| f.relative_path(&picker))
.collect();
paths.sort();
assert_eq!(
paths,
vec!["scope/target.css"],
"regex fallback must not leak outside the FilePath scope"
);
}
+3 -2
View File
@@ -1,5 +1,5 @@
use super::grep::{GrepContext, perform_grep};
use super::prefilter::prefilter_with_filepath_retry;
use super::prefilter::prefilter_files;
use super::sink::{SinkState, debug_assert_newline_terminator};
use super::types::{GrepResult, GrepSearchOptions};
use crate::index::{BigramFilter, BigramOverlay, bigram_boundary, literal_candidates};
@@ -121,7 +121,8 @@ pub(crate) fn multi_grep_search<'a>(
let bigram_candidates = literal_candidates(bigram_index, bigram_overlay, patterns);
let base_file_count = bigram_boundary(bigram_overlay, files.len());
let (files_to_search, filtered_file_count) = prefilter_with_filepath_retry(
// Constraints are separate from patterns, so a miss must not broaden the search.
let (files_to_search, filtered_file_count) = prefilter_files(
files,
constraints,
bigram_candidates.as_deref(),
+1 -1
View File
@@ -50,7 +50,7 @@ pub(super) fn prefilter_with_filepath_retry<'a>(
/// Single pass prefilter that doesn't involve file reading
/// allocates only amount of memory required for storing references of the FileItems have to be
/// opened for grepping unaviodably, in the worst case allocates N * <word> memory if no prefilter needed
fn prefilter_files<'a>(
pub(crate) fn prefilter_files<'a>(
files: &'a [FileItem],
constraints: &[Constraint<'_>],
bigram_candidates: Option<&[u64]>,
+4
View File
@@ -152,6 +152,9 @@ pub struct GrepResult<'a> {
/// literal matching and this field contains the compilation error message.
/// The UI can display this to inform the user their regex was invalid.
pub regex_fallback_error: Option<String>,
/// Set to `true` if the constrained query found nothing and the results come from
/// retrying the whole raw query as literal text (ignored all the inferred constraints)
pub literal_fallback: bool,
}
impl<'a> GrepResult<'a> {
@@ -234,6 +237,7 @@ impl<'a> GrepResult<'a> {
filtered_file_count,
next_file_offset,
regex_fallback_error: None,
literal_fallback: false,
}
}
}
+76 -8
View File
@@ -3,28 +3,53 @@ use std::path::Path;
/// Directories excluded when walking a non-git root. Entries are `cfg`-gated
/// so a single iteration covers standard + platform-specific overrides.
pub(crate) const IGNORED_DIRS: &[&str] = &[
// various dev tools that can be meet in the developer app
"node_modules",
"__pycache__",
"venv",
".venv",
// Rust (glob-only patterns for non_git_repo_overrides; is_non_code_directory
// matches the "target" component separately).
"target/debug",
"target/release",
"target/rust-analyzer",
"target/criterion",
// Language package caches in non-git roots.
"go/pkg/mod",
".cargo/registry",
".rustup/toolchains",
".gradle/caches",
".m2/repository",
".npm/_cacache",
".pub-cache",
#[cfg(not(target_os = "windows"))]
".local/state", // this contains tons of logs which generate too much watcher noise
#[cfg(target_os = "macos")]
"Library/Application Support",
#[cfg(target_os = "macos")]
"Library/Caches",
// App-group sandbox storage — used by iMessage, Photos, Notes, Calendar,
// Electron apps, etc. for SQLite-WAL, LevelDB, protobuf files. These are
// almost entirely extension-less binary files (~80k on a typical $HOME)
// that never need to appear in a fuzzy or grep search.
#[cfg(target_os = "macos")]
"Library/Group Containers",
"Library/Containers", // sandboxed apps data
#[cfg(target_os = "macos")]
"Library/Containers",
"Library/Group Containers", // random application data and networking
#[cfg(target_os = "macos")]
"Library/pnpm",
#[cfg(target_os = "macos")]
"Library/Metadata",
#[cfg(target_os = "macos")]
"Library/Developer/CoreSimulator",
#[cfg(target_os = "macos")]
"Library/Android",
#[cfg(target_os = "macos")]
"Library/Logs",
#[cfg(target_os = "macos")]
"Library/Daemon Containers",
#[cfg(target_os = "macos")]
"Library/Trial",
#[cfg(target_os = "macos")]
"Library/Preferences",
#[cfg(target_os = "macos")]
"Library/Messages",
#[cfg(target_os = "macos")]
"Library/IdentityServices",
#[cfg(target_os = "windows")]
"bin/Debug",
#[cfg(target_os = "windows")]
@@ -57,6 +82,10 @@ pub(crate) fn non_git_repo_overrides(base_path: &Path) -> Option<ignore::overrid
pub(crate) fn is_non_code_directory(path: &Path) -> bool {
let path_str = path.as_os_str().to_str().unwrap_or("");
IGNORED_DIRS.iter().any(|&dir| {
// Entries are gitignore patterns for the walkers; here they are matched
// as substrings, so a leading `*` wildcard has to come off first.
let dir = dir.strip_prefix('*').unwrap_or(dir);
#[cfg(target_os = "windows")]
let dir = dir.replace('/', std::path::MAIN_SEPARATOR_STR);
#[cfg(target_os = "windows")]
@@ -66,3 +95,42 @@ pub(crate) fn is_non_code_directory(path: &Path) -> bool {
path_str.contains(dir)
})
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn home_machine_state_is_excluded_but_source_trees_are_not() {
// Representative machine state from a home index.
for rel in [
"Library/pnpm/store/v3/files/00/abcdef",
"Library/Preferences/com.apple.finder.plist",
"Library/Messages/prewarm.db-shm",
"Library/IdentityServices/TetraDB-identityservicesd.db-wal",
"Library/Developer/CoreSimulator/Devices/X/data/f",
"go/pkg/mod/github.com/x/y@v1/main.go",
".cargo/registry/src/index.crates.io-1/serde-1.0/src/lib.rs",
"Library/Android/sdk/platforms/android-34/data/x",
".local/state/nvim/fff+123+456.log",
] {
assert!(
is_non_code_directory(Path::new(rel)),
"{rel} must not reach the index"
);
}
// Source trees under $HOME stay searchable.
for rel in [
"dev/chromium/third_party/blink/renderer/core/dom/node.cc",
"dev/fff/crates/fff-core/src/lib.rs",
"Documents/notes/todo.md",
"dev/myproj/pkg/mod/thing.go",
] {
assert!(
!is_non_code_directory(Path::new(rel)),
"{rel} must stay searchable"
);
}
}
}
+2 -1
View File
@@ -264,7 +264,8 @@ impl BigramIndexBuilder {
fn flush_seen(&self, seen: &[u64; SEEN_WORDS], word_idx: usize, bit_mask: u64) {
let col_base = self.col_data_ptr();
let words = self.words;
for (blk, block) in seen.chunks_exact(8).enumerate() {
// SEEN_WORDS is a multiple of 8, so the remainder is always empty.
for (blk, block) in seen.as_chunks::<8>().0.iter().enumerate() {
// OR-test whole blocks so the mostly-empty bitmap scans fast.
if block.iter().fold(0u64, |a, &w| a | w) == 0 {
continue;
+7 -1
View File
@@ -1,6 +1,6 @@
//! # FFF Search — High-performance file finder core
//!
//! This crate provides the core search engine for [FFF (Fast File Finder)](https://github.com/dmtrKovalenko/fff.nvim).
//! This crate provides the core search engine for [FFF (Fast File Finder)](https://github.com/dmtrKovalenko/fff).
//! It includes filesystem indexing with real-time watching, fuzzy matching powered
//! by [frizbee](https://docs.rs/neo_frizbee), frecency scoring backed by LMDB,
//! and multi-mode grep search.
@@ -135,6 +135,12 @@ pub use types::*;
pub mod constants;
/// Watcher rescan request accounting.
pub mod rescan_stats;
pub use rescan_stats::{RESCAN_STATS_ENABLED, RescanReason, RescanStats};
mod rescan_throttle;
// ==================================
// these are public only for benchmarks, no backward compatibility guaranteed
#[doc(hidden)]
+215
View File
@@ -0,0 +1,215 @@
#[cfg(rescan_stats)]
use std::sync::atomic::{AtomicUsize, Ordering};
/// Whether rescan accounting is compiled in.
pub const RESCAN_STATS_ENABLED: bool = cfg!(rescan_stats);
/// Cause recorded for a filesystem rescan request.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RescanReason {
/// Requested through the public API (refresh, directory change).
Explicit,
/// The kernel dropped events and asked us to re-read the subtree.
KernelEventLoss,
/// A `.gitignore`/`.ignore` changed, so the cached ignore rules are stale.
IgnoreFileChanged,
/// A single debounce batch touched more paths than we apply incrementally.
EventBatchOverflow,
/// The picker refused an incremental insert/update.
IndexUpdateRejected,
/// The post-scan overflow region ran out of slots.
OverflowCapacity,
}
impl RescanReason {
pub const ALL: [RescanReason; 6] = [
RescanReason::Explicit,
RescanReason::KernelEventLoss,
RescanReason::IgnoreFileChanged,
RescanReason::EventBatchOverflow,
RescanReason::IndexUpdateRejected,
RescanReason::OverflowCapacity,
];
pub const fn as_str(self) -> &'static str {
match self {
RescanReason::Explicit => "explicit",
RescanReason::KernelEventLoss => "kernel_event_loss",
RescanReason::IgnoreFileChanged => "ignore_file_changed",
RescanReason::EventBatchOverflow => "event_batch_overflow",
RescanReason::IndexUpdateRejected => "index_update_rejected",
RescanReason::OverflowCapacity => "overflow_capacity",
}
}
const fn slot(self) -> usize {
match self {
RescanReason::Explicit => 0,
RescanReason::KernelEventLoss => 1,
RescanReason::IgnoreFileChanged => 2,
RescanReason::EventBatchOverflow => 3,
RescanReason::IndexUpdateRejected => 4,
RescanReason::OverflowCapacity => 5,
}
}
}
impl std::fmt::Display for RescanReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Snapshot of rescan requests grouped by reason.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RescanStats {
pub total: usize,
/// Requests suppressed during the cooldown.
pub throttled: usize,
counts: [usize; RescanReason::ALL.len()],
throttled_counts: [usize; RescanReason::ALL.len()],
}
impl RescanStats {
pub fn count(&self, reason: RescanReason) -> usize {
self.counts[reason.slot()]
}
pub fn count_throttled(&self, reason: RescanReason) -> usize {
self.throttled_counts[reason.slot()]
}
/// Admitted requests originating from watcher fallbacks.
pub fn watcher_triggered(&self) -> usize {
self.total - self.count(RescanReason::Explicit)
}
/// Per-reason delta against an earlier snapshot.
pub fn since(&self, earlier: &RescanStats) -> RescanStats {
let mut counts = [0usize; RescanReason::ALL.len()];
let mut throttled_counts = [0usize; RescanReason::ALL.len()];
for slot in 0..RescanReason::ALL.len() {
counts[slot] = self.counts[slot].saturating_sub(earlier.counts[slot]);
throttled_counts[slot] =
self.throttled_counts[slot].saturating_sub(earlier.throttled_counts[slot]);
}
RescanStats {
total: self.total.saturating_sub(earlier.total),
throttled: self.throttled.saturating_sub(earlier.throttled),
counts,
throttled_counts,
}
}
}
impl std::fmt::Display for RescanStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} rescan(s)", self.total)?;
let mut first = true;
for reason in RescanReason::ALL {
let count = self.count(reason);
if count == 0 {
continue;
}
f.write_str(if first { " [" } else { ", " })?;
write!(f, "{reason}={count}")?;
first = false;
}
if !first {
f.write_str("]")?;
}
if self.throttled > 0 {
write!(f, ", {} throttled", self.throttled)?;
}
Ok(())
}
}
#[cfg(rescan_stats)]
#[derive(Default)]
pub(crate) struct RescanCounters {
counters: [AtomicUsize; RescanReason::ALL.len()],
throttled: [AtomicUsize; RescanReason::ALL.len()],
}
#[cfg(rescan_stats)]
impl RescanCounters {
pub(crate) fn record(&self, reason: RescanReason) {
self.counters[reason.slot()].fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_throttled(&self, reason: RescanReason) {
self.throttled[reason.slot()].fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn snapshot(&self) -> RescanStats {
let mut stats = RescanStats::default();
for reason in RescanReason::ALL {
let count = self.counters[reason.slot()].load(Ordering::Relaxed);
stats.counts[reason.slot()] = count;
stats.total += count;
let throttled = self.throttled[reason.slot()].load(Ordering::Relaxed);
stats.throttled_counts[reason.slot()] = throttled;
stats.throttled += throttled;
}
stats
}
pub(crate) fn reset(&self) {
for counter in self.counters.iter().chain(self.throttled.iter()) {
counter.store(0, Ordering::Relaxed);
}
}
}
// Release builds retain the API without counter storage.
#[cfg(not(rescan_stats))]
#[derive(Default)]
pub(crate) struct RescanCounters;
#[cfg(not(rescan_stats))]
impl RescanCounters {
pub(crate) fn record(&self, _reason: RescanReason) {}
pub(crate) fn record_throttled(&self, _reason: RescanReason) {}
pub(crate) fn snapshot(&self) -> RescanStats {
RescanStats::default()
}
pub(crate) fn reset(&self) {}
}
#[cfg(all(test, rescan_stats))]
mod tests {
use super::*;
#[test]
fn counters_attribute_and_diff_per_reason() {
let counters = RescanCounters::default();
counters.record(RescanReason::Explicit);
let baseline = counters.snapshot();
counters.record(RescanReason::IgnoreFileChanged);
counters.record(RescanReason::IgnoreFileChanged);
counters.record(RescanReason::OverflowCapacity);
let stats = counters.snapshot();
assert_eq!(stats.total, 4);
assert_eq!(stats.watcher_triggered(), 3);
let delta = stats.since(&baseline);
assert_eq!(delta.total, 3);
assert_eq!(delta.count(RescanReason::Explicit), 0);
assert_eq!(delta.count(RescanReason::IgnoreFileChanged), 2);
assert_eq!(
delta.to_string(),
"3 rescan(s) [ignore_file_changed=2, overflow_capacity=1]"
);
counters.reset();
assert_eq!(counters.snapshot(), RescanStats::default());
}
}
+124
View File
@@ -0,0 +1,124 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use crate::constants::{
LARGE_INDEX_FILE_COUNT, RESCAN_MIN_INTERVAL, RESCAN_MIN_INTERVAL_LARGE_INDEX,
};
const NEVER: u64 = u64::MAX;
// Drops watcher rescan requests inside the cooldown after the last scan.
// A slightly stale index is fine: the next admitted event rescans everything.
pub(crate) struct RescanThrottle {
epoch: Instant,
last_admitted: AtomicU64,
}
impl Default for RescanThrottle {
fn default() -> Self {
Self {
epoch: Instant::now(),
last_admitted: AtomicU64::new(NEVER),
}
}
}
impl RescanThrottle {
/// Returns `true` if a rescan may start now and records it as the last scan
pub(crate) fn admit(&self, live_files: usize, has_git_repo: bool) -> bool {
let min_interval = if !has_git_repo && live_files >= LARGE_INDEX_FILE_COUNT {
RESCAN_MIN_INTERVAL_LARGE_INDEX
} else {
RESCAN_MIN_INTERVAL
};
let min_ms = min_interval.as_millis() as u64;
let now = self.elapsed_ms();
loop {
let last = self.last_admitted.load(Ordering::Acquire);
if last != NEVER && now.saturating_sub(last) < min_ms {
return false;
}
// CAS so two concurrent requests cannot both start a walk.
if self
.last_admitted
.compare_exchange(last, now, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return true;
}
}
}
/// Records an explicit (unthrottled) scan so watcher requests right after
/// it are dropped: the index is already fresh.
pub(crate) fn note_explicit_scan(&self) {
self.last_admitted
.store(self.elapsed_ms(), Ordering::Release);
}
fn elapsed_ms(&self) -> u64 {
self.epoch.elapsed().as_millis() as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn throttle_at(ms_ago: u64) -> RescanThrottle {
let now = Instant::now();
RescanThrottle {
epoch: now
.checked_sub(Duration::from_millis(ms_ago))
.expect("monotonic clock older than the rewind"),
last_admitted: AtomicU64::new(0),
}
}
#[test]
fn first_request_is_always_admitted() {
let throttle = RescanThrottle::default();
assert!(throttle.admit(100, true));
}
#[test]
fn requests_inside_the_cooldown_are_dropped() {
let throttle = throttle_at(1_000);
assert!(!throttle.admit(100, false));
assert!(!throttle.admit(100, false));
}
#[test]
fn a_large_index_outside_a_git_repo_uses_the_slower_cadence() {
// A minute is past the normal cooldown but not the large-index one.
let throttle = throttle_at(60_000);
assert!(throttle.admit(100, false));
let throttle = throttle_at(60_000);
assert!(!throttle.admit(LARGE_INDEX_FILE_COUNT, false));
}
#[test]
fn a_git_repo_keeps_the_normal_cadence_at_any_size() {
let throttle = throttle_at(60_000);
assert!(throttle.admit(LARGE_INDEX_FILE_COUNT, true));
}
#[test]
fn cooldown_expiry_admits_again() {
let throttle = throttle_at(RESCAN_MIN_INTERVAL.as_millis() as u64 + 1);
assert!(throttle.admit(100, false));
// Admission rearms the cooldown.
assert!(!throttle.admit(100, false));
}
#[test]
fn explicit_scan_rearms_the_cooldown() {
let throttle = RescanThrottle::default();
throttle.note_explicit_scan();
assert!(!throttle.admit(100, false));
}
}
+75 -5
View File
@@ -8,6 +8,8 @@ use crate::file_picker::FilePicker;
use crate::frecency::FrecencyTracker;
use crate::git::GitStatusCache;
use crate::query_tracker::QueryTracker;
use crate::rescan_stats::{RescanCounters, RescanReason, RescanStats};
use crate::rescan_throttle::RescanThrottle;
use crate::scan::ScanJob;
use crate::watch::{WatchEvent, WatchId, WatchOptions, WatchRegistry};
use git2::Repository;
@@ -77,6 +79,8 @@ pub struct SharedPickerInner {
/// Watch subscriptions live outside the picker lock so delivery and
/// (un)subscribing never contend with searches.
watchers: Arc<WatchRegistry>,
rescans: RescanCounters,
rescan_throttle: RescanThrottle,
}
impl Default for SharedPickerInner {
@@ -84,6 +88,8 @@ impl Default for SharedPickerInner {
Self {
picker: parking_lot::RwLock::new(None),
watchers: Arc::new(WatchRegistry::default()),
rescans: RescanCounters::default(),
rescan_throttle: RescanThrottle::default(),
}
}
}
@@ -199,6 +205,39 @@ impl SharedFilePicker {
/// Performs a safe async rescan. Guarantees only single active rescan per picker.
/// If many rescans requested the last one guaranteed to be finished.
pub fn trigger_full_rescan_async(&self, shared_frecency: &SharedFrecency) -> Result<(), Error> {
self.trigger_full_rescan_with_reason(shared_frecency, RescanReason::Explicit)
.map(|_| ())
}
/// Returns admitted and throttled rescan requests by reason.
/// Counters start at picker creation or the last reset.
pub fn rescan_stats(&self) -> RescanStats {
self.0.rescans.snapshot()
}
pub fn reset_rescan_stats(&self) {
self.0.rescans.reset();
}
/// Returns `Ok(true)` when a rescan was started (or queued behind an
/// active scan) and `Ok(false)` when the request was throttled — the
/// caller must then fall back to incremental event processing.
pub(crate) fn trigger_full_rescan_with_reason(
&self,
shared_frecency: &SharedFrecency,
reason: RescanReason,
) -> Result<bool, Error> {
// for giant folders we have no other choice other than throttling rescans
// if user is running application in millions of files with a ton of rescan events
// we drop / throttle some of requests to avoid constant burst of IO
if reason == RescanReason::Explicit {
self.0.rescan_throttle.note_explicit_scan();
} else if !self.check_rescan_throttle(reason) {
return Ok(false);
}
self.0.rescans.record(reason);
match ScanJob::new_rescan(self, shared_frecency)? {
Some(job) => {
job.spawn();
@@ -219,7 +258,27 @@ impl SharedFilePicker {
}
}
}
Ok(())
Ok(true)
}
fn check_rescan_throttle(&self, reason: RescanReason) -> bool {
let (live_files, has_git) = self
.read()
.ok()
.and_then(|guard| {
guard
.as_ref()
.map(|picker| (picker.live_file_count(), picker.has_git_repo()))
})
.unwrap_or((0, false));
if self.0.rescan_throttle.admit(live_files, has_git) {
return true;
}
self.0.rescans.record_throttled(reason);
tracing::debug!(%reason, live_files, "Rescan throttled, skipping");
false
}
/// Subscribe to filesystem changes matching `pattern`.
@@ -432,20 +491,31 @@ impl<T: LmdbStore> SharedDb<T> {
/// Drop the in-memory tracker and delete the on-disk database directory.
///
/// Acquires the write lock, ensuring all readers (including any active mmap
/// access) are finished before the LMDB environment is closed and the files
/// are removed.
///
/// Returns `Ok(Some(path))` with the deleted path, or `Ok(None)` if no tracker was initialized.
pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
let mut guard = self.write()?;
let Some(tracker) = guard.take() else {
return Ok(None);
};
let closing_event = match tracker.shared_env().destroy() {
Ok(closing) => closing,
Err(e) => {
*guard = Some(tracker);
return Err(e);
}
};
let db_path = tracker.env().path().to_path_buf();
// Drop closes the LMDB env and unmaps the files
drop(tracker);
drop(guard);
// Deleting before mdb_env_close finishes would race the unmap.
if let Some(event) = closing_event {
event.wait_timeout(Duration::from_secs(5));
}
std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
path: db_path.clone(),
source,
+13 -1
View File
@@ -415,7 +415,10 @@ mod tests {
#[test]
fn test_chunked_string_full_path() {
let (store, strings, _files) = build_test_store(&["src/components/Button.tsx"]);
let (store, strings, _files) = build_test_store(&[
"src/components/Button.tsx",
"src/components/Button.test.tsx",
]);
let arena = store.as_arena_ptr();
let cs = &strings[0];
@@ -423,6 +426,15 @@ mod tests {
assert_eq!(cs.read_to_buf(arena, &mut buf), "src/components/Button.tsx");
assert_eq!(cs.byte_len, 25);
assert_eq!(cs.filename_offset, 15);
let cs = &strings[1];
let mut buf = [0u8; 512];
assert_eq!(
cs.read_to_buf(arena, &mut buf),
"src/components/Button.test.tsx"
);
assert_eq!(cs.byte_len, 30);
assert_eq!(cs.filename_offset, 15);
}
#[test]
+2
View File
@@ -20,6 +20,8 @@ pub(crate) use ripgrep::walk_collect_files;
pub(crate) struct WalkOutput {
pub(crate) pairs: Vec<(FileItem, String)>,
/// Every non-ignored directory the walk visited, relative, ending with /
pub(crate) dirs: Vec<String>,
pub(crate) ignore_rules: Option<WalkIgnoreRules>,
}
+19 -4
View File
@@ -34,9 +34,12 @@ pub(crate) fn walk_collect_files(
let walker = walk_builder.build_parallel();
let pairs = parking_lot::Mutex::new(Vec::<(FileItem, String)>::new());
// Single lock for both collections: every entry is either a file or a
// dir, so this keeps one mutex acquisition per entry.
let collected =
parking_lot::Mutex::new((Vec::<(FileItem, String)>::new(), Vec::<String>::new()));
walker.run(|| {
let pairs = &pairs;
let collected = &collected;
let counter = Arc::clone(synced_files_count);
let base_path = base_path.to_path_buf();
@@ -58,15 +61,27 @@ pub(crate) fn walk_collect_files(
let (file_item, rel_path) =
FileItem::new_from_walk(path, &base_path, None, metadata.as_ref());
pairs.lock().push((file_item, rel_path));
collected.lock().0.push((file_item, rel_path));
counter.fetch_add(1, Ordering::Relaxed);
} else if entry.depth() > 0 && entry.file_type().is_some_and(|ft| ft.is_dir()) {
let path = entry.path();
if !is_git_file(path)
&& let Ok(rel) = path.strip_prefix(&base_path)
{
let mut rel = crate::path_utils::to_canonical_slashes(&rel.to_string_lossy())
.into_owned();
rel.push('/');
collected.lock().1.push(rel);
}
}
ignore::WalkState::Continue
})
});
let (pairs, dirs) = collected.into_inner();
Ok(WalkOutput {
pairs: pairs.into_inner(),
pairs,
dirs,
ignore_rules: None,
})
}
+20 -8
View File
@@ -1,10 +1,8 @@
//! Filesystem traversal backed by zlob's native parallel walker.
//! Active when the `zlob` feature is enabled (requires the Zig toolchain).
use crate::file_picker::is_known_binary_extension_basename;
use crate::ignore::IGNORED_DIRS;
use crate::types::FileItem;
use crate::walk::{WalkIgnoreRules, WalkOutput};
use parking_lot::Mutex;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -48,12 +46,25 @@ pub(crate) fn walk_collect_files(
tracing::warn!(?e, "zlob extra_ignore rejected; walking without it");
}
let pairs = parking_lot::Mutex::new(Vec::<(FileItem, String)>::new());
// Single lock for both collections: every entry is either a file or a
// dir, so this keeps one mutex acquisition per entry.
let collected = Mutex::new((Vec::new(), Vec::new()));
let outcome = match builder.run(|entry| {
if !entry.is_file() {
// unlike ripgrep walker zlob doesnt show .git files
if entry.is_dir() {
let rel_bytes = entry.relative_path_bytes();
if !rel_bytes.is_empty() {
let mut rel = String::from_utf8_lossy(rel_bytes).into_owned();
rel.push('/');
collected.lock().1.push(rel);
}
}
return WalkState::Continue;
}
let rel_bytes = entry.relative_path_bytes();
// `basename()` returns `&str` for files only.
@@ -73,9 +84,9 @@ pub(crate) fn walk_collect_files(
let rel_str = String::from_utf8_lossy(rel_bytes).into_owned();
let item = FileItem::new_raw(basename_offset, size, modified, None, is_binary);
let mut guard = pairs.lock();
guard.push((item, rel_str));
let n = guard.len();
let mut guard = collected.lock();
guard.0.push((item, rel_str));
let n = guard.0.len();
drop(guard);
if n % PROGRESS_STEP == 0 {
@@ -93,7 +104,7 @@ pub(crate) fn walk_collect_files(
}
};
let pairs = pairs.into_inner();
let (pairs, dirs) = collected.into_inner();
// Always report the exact final total regardless of the last step.
synced_files_count.store(pairs.len(), Ordering::Relaxed);
@@ -106,6 +117,7 @@ pub(crate) fn walk_collect_files(
Ok(WalkOutput {
pairs,
dirs,
ignore_rules,
})
}
+193 -112
View File
@@ -2,6 +2,7 @@ use crate::constants::MAX_OVERFLOW_FILES;
use crate::error::Error;
use crate::file_picker::FFFMode;
use crate::git_status_worker::GitStatusWorker;
use crate::rescan_stats::RescanReason;
use crate::shared::{SharedFilePicker, SharedFrecency};
use crate::sort_buffer::sort_with_buffer;
use crate::watch::{RawWatchEvent, WatchEventKind};
@@ -22,10 +23,19 @@ type Debouncer = notify_debouncer_full::Debouncer<notify::RecommendedWatcher, No
/// are fully joined before `stop()` / `Drop` returns.
pub struct BackgroundWatcher {
debouncer: Arc<Mutex<Option<Debouncer>>>,
watch_tx: Option<mpsc::Sender<PathBuf>>,
watch_tx: Option<mpsc::Sender<WatchTask>>,
owner_thread: Option<std::thread::JoinHandle<()>>,
}
enum WatchTask {
/// Only subscribe to a specific path, this is happening when we did rescun and have to update
/// the watcher only
Subscribe(PathBuf),
/// This is requires a separate walk of the new directory copies or created within a scan
/// window because it might contain subdirectories we have to walk, prune, and add to index
IndexNewDir(PathBuf),
}
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(50);
/// Minimum seconds between frecency tracks of the same file in AI mode.
/// Prevents score inflation from rapid burst edits by AI agents.
@@ -77,7 +87,7 @@ impl BackgroundWatcher {
// spare watcher (configurable by the user, usually 100k - 1m)
let use_recursive = cfg!(any(target_os = "macos", target_os = "windows"));
let (watch_tx, watch_rx) = mpsc::channel::<PathBuf>();
let (watch_tx, watch_rx) = mpsc::channel::<WatchTask>();
let watch_tx_for_debouncer = watch_tx.clone();
let owner_weak_picker = shared_picker.weaken();
@@ -108,48 +118,46 @@ impl BackgroundWatcher {
.name("fff-watcher-own".into())
.spawn(move || {
let _g = owner_span.enter();
while let Ok(dir) = watch_rx.recv() {
while let Ok(task) = watch_rx.recv() {
// if the picker is dropped we do need to exit the loop
let Some(strong_picker) = owner_weak_picker.upgrade() else {
break;
};
// Only inotify (Linux) has no kernel-level recursion, so
// it's the only platform that needs a per-subdir watch to
// be registered at runtime. macOS FSEvents and Windows
// ReadDirectoryChangesW are already watching recursively
// from the base path (see `create_debouncer`), and
// registering a second overlapping stream there produces
// duplicate/out-of-order events.
#[cfg(target_os = "linux")]
{
// Register the new directory with the debouncer, then
// drop the mutex BEFORE doing picker-side work — see
// the comment on `BackgroundWatcher::stop` for the
// lock-ordering rationale.
let mut guard = owner_debouncer.lock();
let Some(debouncer) = guard.as_mut() else {
break;
};
let (dir, is_new_dir) = match task {
WatchTask::Subscribe(dir) => (dir, false),
WatchTask::IndexNewDir(dir) => (dir, true),
};
if let Err(e) = debouncer.watch(&dir, RecursiveMode::NonRecursive) {
warn!(
?e,
dir = %dir.display(),
"Failed to init watcher for new directory"
);
}
// Register the watch BEFORE walking so files created mid-walk still handled
#[cfg(target_os = "linux")]
if !watch_dirs_nonrecursive(&owner_debouncer, std::iter::once(dir.as_path())) {
break;
}
track_files_from_new_directories(
&dir,
&strong_picker,
&owner_git_workdir,
&owner_git_worker,
);
if is_new_dir {
// need to call this on every platform to add subdirectories from the
// new folders to the picker, but on linux we have to handle the subdirs
let subdirs = index_new_directory(
&dir,
&strong_picker,
&owner_git_workdir,
&owner_git_worker,
);
// Transient strong ref drops here, back
// to weak-only before the next `recv()`.
// on linux we manually resubscribe for new inodes
#[cfg(target_os = "linux")]
if !watch_dirs_nonrecursive(
&owner_debouncer,
subdirs.iter().map(|p| p.as_path()),
) {
break;
}
drop(subdirs); // need it cause subdirs is unused on non-linux target'
}
drop(strong_picker);
}
tracing::info!("Background watcher is stopped");
@@ -171,7 +179,7 @@ impl BackgroundWatcher {
shared_frecency: SharedFrecency,
mode: FFFMode,
use_recursive: bool,
watch_tx: mpsc::Sender<PathBuf>,
watch_tx: mpsc::Sender<WatchTask>,
git_status_worker: Arc<GitStatusWorker>,
) -> Result<Debouncer, Error> {
let config = Config::default()
@@ -206,7 +214,7 @@ impl BackgroundWatcher {
// every new directory created has to be reflected in the picker state
for dir in new_dirs {
if let Err(e) = watch_tx.send(dir) {
if let Err(e) = watch_tx.send(WatchTask::IndexNewDir(dir)) {
error!(?e, "Failed to send directory update error");
}
}
@@ -304,7 +312,7 @@ impl BackgroundWatcher {
pub(crate) fn request_watch_dir(&self, dir: PathBuf) -> bool {
match self.watch_tx.as_ref() {
Some(tx) => tx.send(dir).is_ok(),
Some(tx) => tx.send(WatchTask::Subscribe(dir)).is_ok(),
None => false,
}
}
@@ -317,7 +325,7 @@ impl Drop for BackgroundWatcher {
}
#[tracing::instrument(name = "fs_events", skip(events, shared_picker, shared_frecency, git_status_worker), level = Level::DEBUG)]
fn handle_debounced_events(
pub(crate) fn handle_debounced_events(
mode: FFFMode,
events: Vec<DebouncedEvent>,
base_path: &Path,
@@ -335,8 +343,8 @@ fn handle_debounced_events(
.ok()
.and_then(|g| g.as_ref().and_then(|p| p.ignore_rules()));
let filter = IgnoreFilter::new(base_path, walker_rules, repo.as_ref());
let mut need_full_rescan = false;
let mut need_full_git_rescan = false;
let mut batch_overflow_attempted = false;
let mut paths_to_remove = Vec::new();
let mut dirs_to_remove: Vec<PathBuf> = Vec::new();
let mut paths_to_add_or_modify = Vec::new();
@@ -346,6 +354,21 @@ fn handle_debounced_events(
let watch_registry = shared_picker.watch_registry();
let need_events_propagation = watch_registry.is_active();
let try_trigger_full_rescan = |reason: RescanReason| -> bool {
match shared_picker.trigger_full_rescan_with_reason(shared_frecency, reason) {
Ok(true) => {
warn!(%reason, "Triggering full rescan");
watch_registry.dispatch_rescan(base_path);
true
}
Ok(false) => false,
Err(e) => {
error!(%reason, "Failed to trigger full rescan: {:?}", e);
false
}
}
};
for debounced_event in &events {
// It is very important to not react to the access errors because we inevitably
// gonna trigger the sync by our own preview or other unnecessary noise
@@ -363,22 +386,19 @@ fn handle_debounced_events(
// When macOS FSEvents (or other backends) overflow their event buffer, the kernel
// drops individual events and emits a rescan flag telling us to re-scan the subtree
if debounced_event.event.need_rescan() {
if debounced_event.event.paths.len() < 16 // this should be usually one event
let small_and_known = debounced_event.event.paths.len() < 16 // this should be usually one event
&& debounced_event
.paths
.iter()
// but we are smart enough and not falling into the paths
.all(|p| !p.is_dir() && !filter.is_ignored(p))
{
break;
.all(|p| !p.is_dir() && !filter.is_ignored(p));
if !small_and_known && try_trigger_full_rescan(RescanReason::KernelEventLoss) {
return Vec::new();
}
warn!(
"Received rescan event for paths {:?}, triggering full rescan",
debounced_event.event.paths
);
need_full_rescan = true;
break;
// Small batches and throttled rescans fall through: the listed
// paths are still applied incrementally below.
}
tracing::debug!(event = ?debounced_event.event, "Processing FS event");
@@ -387,13 +407,24 @@ fn handle_debounced_events(
path.file_name().and_then(|f| f.to_str()),
Some(".ignore") | Some(".gitignore")
) {
if path
.parent()
.is_some_and(|parent| filter.is_ignored(parent))
{
continue;
}
info!(
"Detected change in ignore definition file: {}",
path.display()
);
need_full_rescan = true;
break;
if try_trigger_full_rescan(RescanReason::IgnoreFileChanged) {
return Vec::new();
}
// Throttled: fall through so the ignore file itself stays
// indexed; the stale rules heal on the next admitted rescan.
}
if is_dotgit_change_affecting_status(path, &repo) {
@@ -455,29 +486,18 @@ fn handle_debounced_events(
}
affected_paths_count += debounced_event.event.paths.len();
if affected_paths_count > MAX_OVERFLOW_FILES {
if !batch_overflow_attempted && affected_paths_count > MAX_OVERFLOW_FILES * 4 {
batch_overflow_attempted = true;
warn!(
?affected_paths_count,
max = MAX_OVERFLOW_FILES,
max = MAX_OVERFLOW_FILES * 4,
"Too many affected paths in a single batch, triggering full rescan",
);
need_full_rescan = true;
break;
if try_trigger_full_rescan(RescanReason::EventBatchOverflow) {
return Vec::new();
}
}
if need_full_rescan {
break;
}
}
if need_full_rescan {
info!(?affected_paths_count, "Triggering full rescan");
watch_registry.dispatch_rescan(base_path);
if let Err(e) = shared_picker.trigger_full_rescan_async(shared_frecency) {
error!("Failed to trigger full rescan: {:?}", e);
}
return Vec::new();
}
// It's important to get the allocated sort
@@ -504,7 +524,7 @@ fn handle_debounced_events(
}
let mut files_to_update_git_status = Vec::new();
let mut need_full_rescan = false;
let mut index_update_rejected = false;
let mut overflow_count = 0;
let mut removed_from_dirs = Vec::new();
let mut watch_events = ahash::AHashMap::new();
@@ -565,6 +585,13 @@ fn handle_debounced_events(
files_to_update_git_status.reserve(paths_to_add_or_modify.len());
for path in &paths_to_add_or_modify {
if picker.get_overflow_files().len() >= MAX_OVERFLOW_FILES
&& picker.get_file_by_path(path).is_none()
{
index_update_rejected = true;
break;
}
let existed = need_events_propagation && picker.get_file_by_path(path).is_some();
if picker.handle_create_or_modify(path).is_some() {
@@ -579,7 +606,7 @@ fn handle_debounced_events(
watch_events.insert(path.to_path_buf(), kind);
}
} else {
need_full_rescan = true;
index_update_rejected = true;
}
}
@@ -591,13 +618,22 @@ fn handle_debounced_events(
overflow_count, "File index changes applied",
);
if need_full_rescan || overflow_count > MAX_OVERFLOW_FILES {
info!("Watcher faced limit of index overflow. Triggering rescan");
watch_registry.dispatch_rescan(base_path);
if let Err(e) = shared_picker.trigger_full_rescan_async(shared_frecency) {
error!("Failed to trigger full rescan: {:?}", e);
}
} else if need_events_propagation {
let rescan_started = if index_update_rejected || overflow_count > MAX_OVERFLOW_FILES {
let reason = if index_update_rejected {
RescanReason::IndexUpdateRejected
} else {
RescanReason::OverflowCapacity
};
info!(%reason, "Watcher faced limit of index overflow. Triggering rescan");
try_trigger_full_rescan(reason)
} else {
false
};
// When the rescan is throttled the incrementally applied changes are
// still the freshest state we have — propagate them to subscribers.
if !rescan_started && need_events_propagation {
watch_registry.dispatch(
base_path,
watch_events
@@ -657,7 +693,7 @@ fn handle_debounced_events(
// do not try to update the paths if we anyway going to rescan everything from scratch
// no repo => no consumer thread, so don't accumulate paths nobody will drain
if !need_full_rescan && repo.is_some() {
if !index_update_rejected && repo.is_some() {
if need_full_git_rescan {
// A full git rescan re-reads every tracked path (including ones that just
// went clean after a commit), so it already subsumes the per-path update.
@@ -670,58 +706,78 @@ fn handle_debounced_events(
new_dirs_to_watch
}
/// After registering a watch on a newly created directory, list its
/// immediate children and add any files to the picker.
fn track_files_from_new_directories(
fn index_new_directory(
dir: &Path,
shared_picker: &SharedFilePicker,
git_workdir: &Option<PathBuf>,
git_status_worker: &Arc<GitStatusWorker>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
) -> Vec<PathBuf> {
let repo = git_workdir.as_ref().and_then(|p| Repository::open(p).ok());
// Prefer the walker's ignore rules; read base_path + rules from the picker.
let (base_path, walker_rules) = match shared_picker.read().ok().and_then(|g| {
g.as_ref()
.map(|p| (p.base_path().to_path_buf(), p.ignore_rules()))
let (base_path, walker_rules, follow_symlinks) = match shared_picker.read().ok().and_then(|g| {
g.as_ref().map(|p| {
(
p.base_path().to_path_buf(),
p.ignore_rules(),
p.follows_symlinks(),
)
})
}) {
Some(pair) => pair,
None => return,
Some(triple) => triple,
None => return Vec::new(),
};
let filter = IgnoreFilter::new(&base_path, walker_rules, repo.as_ref());
let mut files_to_add = Vec::new();
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|ft| ft.is_file()) {
let path = entry.path();
// file_type() already ruled out directories — only ignore rules left
if !filter.is_ignored(&path) {
files_to_add.push(path);
}
let walk = match crate::walk::walk_collect_files(
dir,
repo.is_some(),
follow_symlinks,
1,
&Arc::new(std::sync::atomic::AtomicUsize::new(0)),
) {
Ok(walk) => walk,
Err(e) => {
warn!(?e, dir = %dir.display(), "Failed to walk new directory");
return Vec::new();
}
}
};
// TODO: figure out a better optimized way for zlob to rerun the directory walk using existing
// ignore rules, but currently we have to filter out ignored files on our own
let filter = IgnoreFilter::new(&base_path, walker_rules, repo.as_ref());
let join_unless_ignored = |relative_path: &str| -> Option<PathBuf> {
let path = dir.join(relative_path);
(!filter.is_ignored(&path)).then_some(path)
};
let files_to_add: Vec<PathBuf> = walk
.pairs
.iter()
.filter_map(|(_, path)| join_unless_ignored(path))
.collect();
let subdirs: Vec<PathBuf> = walk
.dirs
.iter()
.filter_map(|path| join_unless_ignored(path.trim_end_matches('/')))
.collect();
if files_to_add.is_empty() {
return;
return subdirs;
}
let mut indexed_files = Vec::with_capacity(files_to_add.len());
{
let Ok(mut guard) = shared_picker.write() else {
return;
return subdirs;
};
let Some(ref mut picker) = *guard else {
return;
return subdirs;
};
for path in &files_to_add {
if picker.handle_create_or_modify(path).is_some() {
indexed_files.push(path.clone());
for path in files_to_add {
if picker.handle_create_or_modify(&path).is_some() {
indexed_files.push(path);
}
}
}
@@ -746,10 +802,35 @@ fn track_files_from_new_directories(
}
debug!(
"Injected {} existing files from new directory {}",
"Indexed new {} files from new directory {}",
added,
dir.display(),
);
subdirs
}
#[cfg(target_os = "linux")]
fn watch_dirs_nonrecursive<'a>(
debouncer: &Mutex<Option<Debouncer>>,
dirs: impl Iterator<Item = &'a Path>,
) -> bool {
let mut guard = debouncer.lock();
let Some(debouncer) = guard.as_mut() else {
return false;
};
for dir in dirs {
if let Err(e) = debouncer.watch(dir, RecursiveMode::NonRecursive) {
warn!(
?e,
dir = %dir.display(),
"Failed to init watcher for new directory"
);
}
}
true
}
struct IgnoreFilter<'a> {
@@ -777,13 +858,13 @@ impl<'a> IgnoreFilter<'a> {
/// Whether `path` (absolute) is ignored.
fn is_ignored(&self, path: &Path) -> bool {
if let Some(rules) = self.rules.as_ref() {
let Ok(rel) = path.strip_prefix(self.base_path) else {
let Ok(relative) = path.strip_prefix(self.base_path) else {
return false;
};
// `IgnoreRules::is_ignored` enumerates every ancestor .gitignore
// layer internally, so a leaf under an ignored directory (rule
// `build/`, path `build/out.rs`) is caught in one call.
return rules.is_ignored(rel);
return rules.is_ignored(relative);
}
match self.repo {
Some(repo) => repo.is_path_ignored(path) == Ok(true),
+4
View File
@@ -3,3 +3,7 @@ pub use background_watcher::*;
mod watch;
pub use watch::*;
// The harness reads rescan counters, which release builds compile out.
#[cfg(all(test, rescan_stats))]
mod rescan_tests;
+621
View File
@@ -0,0 +1,621 @@
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::time::{Duration, Instant};
use notify::Event;
use notify::EventKind;
use notify::event::{
AccessKind, AccessMode, CreateKind, DataChange, Flag, ModifyKind, RemoveKind, RenameMode,
};
use notify_debouncer_full::DebouncedEvent;
use tempfile::TempDir;
use super::handle_debounced_events;
use crate::constants::MAX_OVERFLOW_FILES;
use crate::file_picker::{FFFMode, FilePicker, FilePickerOptions};
use crate::git_status_worker::GitStatusWorker;
use crate::rescan_stats::{RescanReason, RescanStats};
use crate::shared::{SharedFilePicker, SharedFrecency};
#[test]
fn saving_an_indexed_file_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
f.write("src/main.rs", "fn main() { println!(); }");
let delta = f.feed([modify(f.path("src/main.rs"))]);
f.assert_no_rescan(&delta, "saving a tracked file");
}
#[test]
fn editor_atomic_save_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
// write-to-temp + rename-over-target, the way vim/VSCode/IntelliJ save.
f.write("src/main.rs", "fn main() { println!(); }");
let target = f.path("src/main.rs");
let temp = f.path("src/.main.rs.swp");
let delta = f.feed([
DebouncedEvent::new(
Event::new(EventKind::Create(CreateKind::File)).add_path(temp.clone()),
Instant::now(),
),
DebouncedEvent::new(
Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::From)))
.add_path(temp.clone()),
Instant::now(),
),
DebouncedEvent::new(
Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::To)))
.add_path(target.clone()),
Instant::now(),
),
DebouncedEvent::new(
Event::new(EventKind::Remove(RemoveKind::File)).add_path(temp),
Instant::now(),
),
]);
f.assert_no_rescan(&delta, "an atomic editor save");
assert!(f.is_indexed("src/main.rs"), "target must stay indexed");
}
#[test]
fn creating_and_deleting_files_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
f.write("src/added.rs", "pub fn added() {}");
let created = f.feed([create(f.path("src/added.rs"))]);
f.assert_no_rescan(&created, "creating a file");
assert!(f.is_indexed("src/added.rs"));
f.remove("src/added.rs");
let removed = f.feed([remove_file(f.path("src/added.rs"))]);
f.assert_no_rescan(&removed, "deleting a file");
assert!(!f.is_indexed("src/added.rs"));
}
#[test]
fn deleting_a_directory_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.write("src/nested/a.rs", "");
f.write("src/nested/b.rs", "");
f.index();
std::fs::remove_dir_all(f.path("src/nested")).unwrap();
let delta = f.feed([DebouncedEvent::new(
Event::new(EventKind::Remove(RemoveKind::Folder)).add_path(f.path("src/nested")),
Instant::now(),
)]);
f.assert_no_rescan(&delta, "deleting a directory");
assert!(!f.is_indexed("src/nested/a.rs"));
assert!(f.is_indexed("src/main.rs"));
}
#[test]
fn read_only_access_events_are_ignored() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
// fff's own preview + grep reads generate these; reacting to them would
// make the picker rescan whenever the user scrolls the result list.
let path = f.path("src/main.rs");
let delta = f.feed([
DebouncedEvent::new(
Event::new(EventKind::Access(AccessKind::Read)).add_path(path.clone()),
Instant::now(),
),
DebouncedEvent::new(
Event::new(EventKind::Access(AccessKind::Open(AccessMode::Read)))
.add_path(path.clone()),
Instant::now(),
),
DebouncedEvent::new(
Event::new(EventKind::Access(AccessKind::Close(AccessMode::Read))).add_path(path),
Instant::now(),
),
]);
f.assert_no_rescan(&delta, "read-only access events");
}
#[test]
fn recreating_the_same_paths_does_not_consume_overflow_capacity() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
// Recreated paths must reuse their overflow slots.
for _ in 0..8 {
for i in 0..200 {
let rel = format!("gen/out{i}.rs");
f.write(&rel, "generated");
f.feed([create(f.path(&rel))]);
}
for i in 0..200 {
let rel = format!("gen/out{i}.rs");
f.remove(&rel);
f.feed([remove_file(f.path(&rel))]);
}
}
let delta = f.all_rescans();
f.assert_no_rescan(&delta, "1600 create/delete cycles over 200 stable paths");
assert!(
f.overflow_len() <= 200,
"each path must claim one overflow slot at most, got {}",
f.overflow_len()
);
}
#[test]
fn writes_inside_a_gitignored_directory_stay_incremental() {
let f = Fixture::with_git();
f.write(".gitignore", "target/\nnode_modules/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
let mut events = Vec::new();
for i in 0..64 {
let rel = format!("target/debug/artifact{i}.o");
f.write(&rel, "binary");
events.push(create(f.path(&rel)));
}
let delta = f.feed(events);
f.assert_no_rescan(&delta, "build output written into an ignored directory");
}
#[test]
fn ignored_event_batch_above_index_capacity_stays_incremental() {
let f = Fixture::with_git();
f.write(".gitignore", "node_modules/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
let events = (0..MAX_OVERFLOW_FILES + 1)
.map(|i| {
let rel = format!("node_modules/pkg/file{i}.js");
f.write(&rel, "");
create(f.path(&rel))
})
.collect::<Vec<_>>();
let delta = f.feed(events);
f.assert_no_rescan(&delta, "ignored events above the index capacity");
assert_eq!(f.overflow_len(), 0);
}
#[test]
fn repeated_edits_above_index_capacity_stay_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let path = f.path("src/main.rs");
let events = (0..MAX_OVERFLOW_FILES + 1)
.map(|_| modify(path.clone()))
.collect::<Vec<_>>();
let delta = f.feed(events);
f.assert_no_rescan(&delta, "repeated edits above the index capacity");
assert_eq!(f.overflow_len(), 0);
}
#[test]
fn ignore_file_inside_an_ignored_directory_stays_incremental() {
let f = Fixture::with_git();
f.write(".gitignore", "node_modules/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
let ignore_files =
["left-pad", "lodash", "typescript"].map(|pkg| format!("node_modules/{pkg}/.gitignore"));
for rel in &ignore_files {
f.write(rel, "dist\n");
}
let delta = f.feed(ignore_files.iter().map(|rel| create(f.path(rel))));
f.assert_no_rescan(&delta, "creating ignored .gitignore files");
for rel in &ignore_files {
f.write(rel, "build\n");
}
let delta = f.feed(ignore_files.iter().map(|rel| modify(f.path(rel))));
f.assert_no_rescan(&delta, "modifying ignored .gitignore files");
for rel in &ignore_files {
f.remove(rel);
}
let delta = f.feed(ignore_files.iter().map(|rel| remove_file(f.path(rel))));
f.assert_no_rescan(&delta, "removing ignored .gitignore files");
}
#[test]
fn ignore_file_inside_an_indexed_directory_triggers_a_rescan() {
let f = Fixture::with_git();
f.write("src/.gitignore", ".gitignore\ngenerated/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
f.write("src/.gitignore", ".gitignore\ngenerated/\nbuild/\n");
let delta = f.feed([modify(f.path("src/.gitignore"))]);
assert_eq!(delta.count(RescanReason::IgnoreFileChanged), 1);
}
#[test]
fn git_internal_churn_stays_incremental() {
let f = Fixture::with_git();
f.write("src/main.rs", "fn main() {}");
f.index();
let git_dir = f.path(".git");
let delta = f.feed([
create(git_dir.join("index.lock")),
modify(git_dir.join("index")),
remove_file(git_dir.join("index.lock")),
modify(git_dir.join("HEAD")),
modify(git_dir.join("logs/HEAD")),
modify(git_dir.join("COMMIT_EDITMSG")),
modify(git_dir.join("refs/heads/main")),
]);
f.assert_no_rescan(&delta, "git writing its own metadata");
}
#[test]
fn changing_the_root_ignore_file_triggers_a_rescan() {
let f = Fixture::with_git();
f.write(".gitignore", "target/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
f.write(".gitignore", "target/\nsrc/\n");
let delta = f.feed([modify(f.path(".gitignore"))]);
assert_eq!(
delta.count(RescanReason::IgnoreFileChanged),
1,
"the indexed set depends on the root ignore rules, got {delta}"
);
}
#[test]
fn kernel_event_loss_on_a_directory_triggers_a_rescan() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let delta = f.feed([DebouncedEvent::new(
Event::new(EventKind::Modify(ModifyKind::Any))
.add_path(f.path("src"))
.set_flag(Flag::Rescan),
Instant::now(),
)]);
assert_eq!(
delta.count(RescanReason::KernelEventLoss),
1,
"a dropped-events flag over a directory means unknown subtree state, got {delta}"
);
}
#[test]
fn new_files_above_index_capacity_trigger_a_rescan() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let events = (0..MAX_OVERFLOW_FILES + 1)
.map(|i| {
let rel = format!("src/bulk{i}.rs");
f.write(&rel, "");
create(f.path(&rel))
})
.collect::<Vec<_>>();
let delta = f.feed(events);
assert_eq!(
delta.count(RescanReason::IndexUpdateRejected),
1,
"new files above the overflow region cannot be applied incrementally, got {delta}"
);
}
#[test]
fn batch_at_the_overflow_boundary_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let events = (0..MAX_OVERFLOW_FILES)
.map(|i| {
let rel = format!("src/bulk{i}.rs");
f.write(&rel, "");
create(f.path(&rel))
})
.collect::<Vec<_>>();
let delta = f.feed(events);
f.assert_no_rescan(&delta, "a batch exactly at the overflow limit");
}
#[test]
fn event_batch_at_four_times_index_capacity_stays_incremental() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let path = f.path("src/main.rs");
let events = (0..MAX_OVERFLOW_FILES * 4)
.map(|_| modify(path.clone()))
.collect::<Vec<_>>();
let delta = f.feed(events);
f.assert_no_rescan(&delta, "an event batch exactly at the event limit");
}
#[test]
fn event_batch_above_four_times_index_capacity_triggers_a_rescan() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
let path = f.path("src/main.rs");
let events = (0..MAX_OVERFLOW_FILES * 4 + 1)
.map(|_| modify(path.clone()))
.collect::<Vec<_>>();
let delta = f.feed(events);
assert_eq!(
delta.count(RescanReason::EventBatchOverflow),
1,
"an event batch above four times the index capacity must rescan, got {delta}"
);
}
#[test]
fn repeated_triggers_inside_the_cooldown_collapse_to_one_rescan() {
let f = Fixture::with_git();
f.write(".gitignore", "target/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
// Repeated batches during the cooldown must share one walk.
for round in 0..50 {
f.write(".gitignore", &format!("target/\n# round {round}\n"));
f.feed([modify(f.path(".gitignore"))]);
}
let stats = f.all_rescans();
assert_eq!(
stats.total, 1,
"50 triggers inside the cooldown must collapse to a single walk, got {stats}"
);
assert_eq!(
stats.throttled, 49,
"every suppressed request must be accounted for, got {stats}"
);
}
#[test]
fn an_explicit_request_is_never_throttled() {
let f = Fixture::with_git();
f.write(".gitignore", "target/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
// Burn the cooldown with a watcher trigger, then confirm a user-initiated
// refresh still goes through.
f.write(".gitignore", "target/\nsrc/\n");
f.feed([modify(f.path(".gitignore"))]);
for _ in 0..3 {
f.picker.trigger_full_rescan_async(&f.frecency).unwrap();
}
let stats = f.all_rescans();
assert_eq!(
stats.count(RescanReason::Explicit),
3,
"explicit refreshes must bypass the throttle, got {stats}"
);
assert_eq!(stats.count_throttled(RescanReason::Explicit), 0);
}
#[test]
fn events_after_a_suppressed_kernel_rescan_are_still_applied() {
let f = Fixture::new();
f.write("src/main.rs", "fn main() {}");
f.index();
f.write("src/added.rs", "pub fn added() {}");
let delta = f.feed([
DebouncedEvent::new(
Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Content)))
.add_path(f.path("src/main.rs"))
.set_flag(Flag::Rescan),
Instant::now(),
),
create(f.path("src/added.rs")),
]);
f.assert_no_rescan(&delta, "a dropped-events flag over a single tracked file");
assert!(
f.is_indexed("src/added.rs"),
"suppressing the rescan must not drop the rest of the batch"
);
}
#[test]
fn a_throttled_ignore_file_event_is_still_applied_incrementally() {
let f = Fixture::with_git();
f.write(".gitignore", "target/\n");
f.write("src/main.rs", "fn main() {}");
f.index();
// Burn the cooldown: deleting .gitignore admits a full rescan.
f.remove(".gitignore");
let delta = f.feed([remove_file(f.path(".gitignore"))]);
assert_eq!(delta.count(RescanReason::IgnoreFileChanged), 1);
f.picker.wait_for_indexing_complete(Duration::from_secs(10));
// Recreating it inside the cooldown throttles the rescan, but the file
// itself must re-enter the index via the incremental fallback.
f.write(".gitignore", "target/\n__ignored_x/\n");
let delta = f.feed([create(f.path(".gitignore"))]);
assert_eq!(delta.total, 0, "the rescan must be throttled, got {delta}");
assert_eq!(delta.count_throttled(RescanReason::IgnoreFileChanged), 1);
assert!(
f.is_indexed(".gitignore"),
"a throttled ignore-file event must still index the file itself"
);
}
struct Fixture {
base: PathBuf,
picker: SharedFilePicker,
frecency: SharedFrecency,
git_workdir: Option<PathBuf>,
git_worker: Arc<GitStatusWorker>,
// Dropped last so background work started by a triggered rescan still
// sees the tree it was asked to walk.
_tmp: TempDir,
}
impl Fixture {
fn new() -> Self {
Self::build(false)
}
fn with_git() -> Self {
Self::build(true)
}
fn build(git: bool) -> Self {
let tmp = tempfile::tempdir().unwrap();
let base = crate::path_utils::canonicalize(tmp.path()).unwrap();
let git_workdir = git.then(|| {
let status = Command::new("git")
.args(["init", "-b", "main"])
.current_dir(&base)
.output()
.expect("git init");
assert!(status.status.success(), "git init failed");
base.clone()
});
Self {
base,
picker: SharedFilePicker::default(),
frecency: SharedFrecency::noop(),
git_workdir,
git_worker: GitStatusWorker::new(),
_tmp: tmp,
}
}
fn index(&self) {
let mut picker = FilePicker::new(FilePickerOptions {
base_path: self.base.to_string_lossy().into_owned(),
watch: false,
..Default::default()
})
.unwrap();
picker.collect_files().unwrap();
self.picker.rebase_watches(&self.base);
*self.picker.write().unwrap() = Some(picker);
}
fn feed(&self, events: impl IntoIterator<Item = DebouncedEvent>) -> RescanStats {
let before = self.picker.rescan_stats();
handle_debounced_events(
FFFMode::Neovim,
events.into_iter().collect(),
&self.base,
&self.git_workdir,
&self.picker,
&self.frecency,
&self.git_worker,
);
self.picker.rescan_stats().since(&before)
}
fn assert_no_rescan(&self, delta: &RescanStats, what: &str) {
assert_eq!(delta.total, 0, "{what} must not trigger a rescan: {delta}");
}
fn path(&self, rel: &str) -> PathBuf {
self.base.join(rel)
}
fn write(&self, rel: &str, contents: &str) {
let path = self.path(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn remove(&self, rel: &str) {
std::fs::remove_file(self.path(rel)).unwrap();
}
fn is_indexed(&self, rel: &str) -> bool {
let guard = self.picker.read().unwrap();
guard
.as_ref()
.and_then(|p| p.get_file_by_path(self.path(rel)))
.is_some_and(|file| !file.is_deleted())
}
fn all_rescans(&self) -> RescanStats {
self.picker.rescan_stats()
}
fn overflow_len(&self) -> usize {
let guard = self.picker.read().unwrap();
guard
.as_ref()
.map(|p| p.get_overflow_files().len())
.unwrap_or(0)
}
}
impl Drop for Fixture {
fn drop(&mut self) {
// A test that intentionally triggers a rescan leaves a walk running on
// the background pool; let it finish before the tree disappears.
self.picker
.wait_for_indexing_complete(Duration::from_secs(10));
}
}
fn event(kind: EventKind, path: PathBuf) -> DebouncedEvent {
DebouncedEvent::new(Event::new(kind).add_path(path), Instant::now())
}
fn create(path: PathBuf) -> DebouncedEvent {
event(EventKind::Create(CreateKind::File), path)
}
fn modify(path: PathBuf) -> DebouncedEvent {
event(
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
path,
)
}
fn remove_file(path: PathBuf) -> DebouncedEvent {
event(EventKind::Remove(RemoveKind::File), path)
}
@@ -254,3 +254,48 @@ fn recreated_directory_reappears_in_dir_search() {
search_dirs(&picker, "phoenix")
);
}
/// Regression for #725: a dir that is EMPTY at scan time must be indexed —
/// searchable in dir search and watched so later file creations are seen.
#[test]
fn empty_directory_at_scan_is_searchable_and_watched() {
let tmp = TempDir::new().unwrap();
let base = fff_search::path_utils::canonicalize(tmp.path()).unwrap();
fs::create_dir_all(base.join("commands")).unwrap();
fs::write(base.join("keep.rs"), "x").unwrap();
let (picker, _frecency) = make_watched_picker(&base);
assert!(
search_dirs(&picker, "commands")
.iter()
.any(|d| d.starts_with("commands")),
"empty dir must be searchable right after the scan, got: {:?}",
search_dirs(&picker, "commands")
);
// The empty dir must reuse its scan-built DirItem when a file lands in it
// and the watcher must have registered a watch on it (the #725 repro).
fs::write(base.join("commands/review.md"), "# review").unwrap();
assert!(
wait_until(
|| {
let guard = picker.read().unwrap();
let p = guard.as_ref().unwrap();
p.get_file_by_path(base.join("commands/review.md"))
.is_some()
},
Duration::from_secs(10)
),
"file created in a scan-time-empty dir must be indexed"
);
let guard = picker.read().unwrap();
let p = guard.as_ref().unwrap();
let commands_dirs = p
.get_dirs()
.iter()
.filter(|d| d.relative_path(p).starts_with("commands"))
.count();
assert_eq!(commands_dirs, 1, "no duplicate DirItem for the empty dir");
}
@@ -773,7 +773,7 @@ fn read_truth_status(base: &Path) -> BTreeMap<String, Status> {
let mut out = BTreeMap::new();
for entry in statuses.iter() {
if let Some(p) = entry.path() {
if let Ok(p) = entry.path() {
// git2 returns forward-slash paths; accept as-is.
out.insert(p.to_string(), entry.status());
}
@@ -1258,7 +1258,7 @@ fn get_baseline_status_from_git(base: &Path) -> Vec<Live> {
Err(_) => return out,
};
for entry in statuses.iter() {
if let Some(p) = entry.path() {
if let Ok(p) = entry.path() {
let abs = base.join(p);
// Must be a real file *right now* — ignore stale WT_DELETED rows.
if abs.is_file() {
+61
View File
@@ -1835,3 +1835,64 @@ fn plain_text_smart_case_finds_uppercase_content_with_lowercase_query() {
"lowercase query should case-insensitively match 'VFIO-KVM'"
);
}
/// Bug pinning: `!=` was parsed as a Not("=") exclusion constraint, dropping it
/// from the needle. Operator tokens must stay literal search text.
#[test]
fn plain_text_not_equals_operator_is_literal() {
let tmp = TempDir::new().unwrap();
let picker = create_picker(
tmp.path(),
&[(
"watch.rs",
"if delivery.sub.epoch.load(Ordering::Acquire) != delivery.epoch {\n",
)],
);
let parsed = parse_grep_query("Ordering::Acquire) != delivery.epoch");
let result = picker.grep(&parsed, &plain_opts());
assert_eq!(
result.matches.len(),
1,
"operator `!=` must match literally"
);
assert!(!result.literal_fallback, "no fallback should be needed");
assert!(result.matches[0].line_content.contains("!= delivery.epoch"));
}
#[test]
fn literal_fallback_when_constraints_find_nothing() {
let tmp = TempDir::new().unwrap();
let picker = create_picker(tmp.path(), &[("a.txt", "foo !bar_baz qux\n")]);
// `!bar_baz` becomes Not(Text) so the constrained needle is "foo qux" → no
// match; the search must retry the raw query as literal text.
let parsed = parse_grep_query("foo !bar_baz qux");
let result = picker.grep(&parsed, &plain_opts());
assert_eq!(result.matches.len(), 1);
assert!(
result.literal_fallback,
"literal fallback should be flagged"
);
assert!(result.matches[0].line_content.contains("!bar_baz"));
}
#[test]
fn literal_fallback_not_triggered_when_constraints_match() {
let tmp = TempDir::new().unwrap();
let picker = create_picker(
tmp.path(),
&[
("src/lib.rs", "needle here\n"),
("test/lib.rs", "needle here\n"),
],
);
let parsed = parse_grep_query("needle !test");
let result = picker.grep(&parsed, &plain_opts());
assert_eq!(result.matches.len(), 1, "exclusion should still apply");
assert!(!result.literal_fallback);
}
+123
View File
@@ -0,0 +1,123 @@
//! One process must be able to hold many trackers over the same LMDB path
//! (issues #700/#760): they share a single pooled env instead of failing
//! with `EnvAlreadyOpened`.
use std::path::{Path, PathBuf};
use fff_search::frecency::FrecencyTracker;
use fff_search::query_tracker::QueryTracker;
use fff_search::shared::SharedFrecency;
fn unique_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("fff-env-pool-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
dir
}
#[test]
fn same_path_trackers_share_one_env() {
let dir = unique_dir("share");
let file = Path::new("/virtual/env-pool/shared.rs");
let a = FrecencyTracker::open(&dir).expect("first open");
let b = FrecencyTracker::open(&dir).expect("second open in the same process (#700/#760)");
a.track_access(file).expect("write via a");
assert_eq!(b.access_count(file).expect("read via b"), 1);
drop(a);
b.track_access(file)
.expect("b must stay usable after a drops");
assert_eq!(b.access_count(file).unwrap(), 2);
drop(b);
let c = FrecencyTracker::open(&dir).expect("reopen after all handles dropped");
assert_eq!(
c.access_count(file).unwrap(),
2,
"data persisted across reopen"
);
drop(c);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn concurrent_open_and_drop_never_collide() {
let dir = unique_dir("hammer");
let file = Path::new("/virtual/env-pool/hammer.rs");
let mut handles = Vec::new();
for t in 0..8 {
let dir = dir.clone();
handles.push(std::thread::spawn(move || {
for i in 0..100 {
let tracker = FrecencyTracker::open(&dir)
.unwrap_or_else(|e| panic!("thread {t} iteration {i}: {e}"));
if i % 20 == 0 {
tracker.track_access(file).expect("track access");
}
}
}));
}
for handle in handles {
handle.join().expect("no thread may panic");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn different_store_on_same_path_is_rejected_with_clear_error() {
let dir = unique_dir("mismatch");
let _frecency = FrecencyTracker::open(&dir).expect("frecency open");
let err = QueryTracker::open(&dir).expect_err("env options differ, must be rejected");
let msg = err.to_string();
assert!(
msg.contains("frecency") && msg.contains("query"),
"error must name both stores so the user can fix their config, got: {msg}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn destroy_refuses_while_shared_then_succeeds_when_sole() {
let dir = unique_dir("destroy");
let file = Path::new("/virtual/env-pool/destroy.rs");
let shared = SharedFrecency::default();
shared
.init(FrecencyTracker::open(&dir).expect("init open"))
.expect("init");
let other = FrecencyTracker::open(&dir).expect("second handle over the same db");
shared
.destroy()
.expect_err("destroy must refuse while another tracker uses the env");
// Refusal must keep both the files and the shared handle intact.
assert!(
dir.join("data.mdb").exists(),
"db files survive a refused destroy"
);
shared
.read()
.expect("read lock")
.as_ref()
.expect("tracker restored after refused destroy")
.track_access(file)
.expect("shared handle still works");
drop(other);
let removed = shared
.destroy()
.expect("sole-owner destroy succeeds")
.expect("a path was removed");
assert!(
!removed.exists(),
"db dir deleted once nobody shares the env"
);
}
@@ -0,0 +1,163 @@
// Repro for #783: fff opens LMDB envs with only map_size set, leaving heed's
// default max_readers (126) and default TLS mode. Long-lived threads each pin a
// reader slot for the thread's lifetime, so >126 live reader threads exhaust the
// table with MDB_READERS_FULL.
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, mpsc};
use std::time::Duration;
use heed::EnvOpenOptions;
// This binary links heed directly without the fff lib, so nothing pulls in
// advapi32 for lmdb's security-descriptor calls in mdb_env_setup_locks.
#[cfg(windows)]
#[link(name = "advapi32")]
unsafe extern "C" {}
fn temp_env_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("fff-readers-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
// Regression for #783: with only map_size set (pre-fix), heed's default 126
// reader slots are exhausted once >126 live threads each hold a read txn. fff now
// raises max_readers, so this many live readers must all get a slot.
const FFF_MAX_READERS: u32 = 1024;
#[test]
fn raised_max_readers_admits_more_than_126_live_readers() {
let dir = temp_env_dir("raised");
let env = unsafe {
EnvOpenOptions::new()
.map_size(10 * 1024 * 1024)
.max_readers(FFF_MAX_READERS)
.open(&dir)
}
.unwrap();
const THREADS: usize = 200;
let stop = Arc::new(AtomicBool::new(false));
let ready = Arc::new(Barrier::new(THREADS + 1));
let readers_full = Arc::new(AtomicBool::new(false));
let (tx, rx) = mpsc::channel::<bool>(); // true = read txn acquired
let mut handles = Vec::new();
for _ in 0..THREADS {
let env = env.clone();
let stop = stop.clone();
let ready = ready.clone();
let readers_full = readers_full.clone();
let tx = tx.clone();
handles.push(std::thread::spawn(move || {
match env.read_txn() {
Ok(txn) => {
tx.send(true).ok();
ready.wait();
while !stop.load(Ordering::Relaxed) {
std::thread::park_timeout(Duration::from_millis(5));
}
drop(txn); // hold the slot for the whole test
}
Err(e) => {
if e.to_string().contains("MDB_READERS_FULL") {
readers_full.store(true, Ordering::Relaxed);
}
tx.send(false).ok();
ready.wait();
}
}
}));
}
drop(tx);
// Collect exactly one result per thread; parked threads keep their tx clone
// alive, so we must not wait for the channel to close.
let acquired = AtomicUsize::new(0);
for _ in 0..THREADS {
if rx.recv().unwrap() {
acquired.fetch_add(1, Ordering::Relaxed);
}
}
ready.wait();
let acquired = acquired.load(Ordering::Relaxed);
stop.store(true, Ordering::Relaxed);
for h in handles {
h.join().unwrap();
}
let _ = std::fs::remove_dir_all(&dir);
// With max_readers raised, all 200 live reader threads must get a slot and
// none may see MDB_READERS_FULL. On the pre-fix default of 126 this plateaus
// at 126 and the rest fail.
assert!(
!readers_full.load(Ordering::Relaxed),
"MDB_READERS_FULL hit: only {acquired}/{THREADS} live reader threads got a slot"
);
assert_eq!(
acquired, THREADS,
"all {THREADS} live reader threads should get a slot; got {acquired}"
);
}
// Structural fix for #783: with MDB_NOTLS a reader slot is tied to the txn
// object and freed on drop, not pinned per thread. 200 long-lived threads each
// open+drop a txn against the *default* 126-slot table; in TLS mode this
// plateaus at 126, in NOTLS mode every thread must succeed.
#[test]
fn notls_releases_slots_of_live_threads() {
let dir = temp_env_dir("notls");
let env = unsafe {
EnvOpenOptions::new()
.read_txn_without_tls()
.map_size(10 * 1024 * 1024)
.open(&dir)
}
.unwrap();
const THREADS: usize = 200;
let stop = Arc::new(AtomicBool::new(false));
let ready = Arc::new(Barrier::new(THREADS + 1));
// Serialize txns so the test measures slot *release*, not concurrency.
let txn_gate = Arc::new(std::sync::Mutex::new(()));
let acquired = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..THREADS {
let env = env.clone();
let stop = stop.clone();
let ready = ready.clone();
let txn_gate = txn_gate.clone();
let acquired = acquired.clone();
handles.push(std::thread::spawn(move || {
{
let _gate = txn_gate.lock().unwrap();
if let Ok(txn) = env.read_txn() {
acquired.fetch_add(1, Ordering::Relaxed);
drop(txn); // NOTLS: slot returns to the pool here
}
}
// Stay alive: in TLS mode this thread would keep its slot pinned.
ready.wait();
while !stop.load(Ordering::Relaxed) {
std::thread::park_timeout(Duration::from_millis(5));
}
}));
}
ready.wait();
let got = acquired.load(Ordering::Relaxed);
stop.store(true, Ordering::Relaxed);
for h in handles {
h.join().unwrap();
}
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
got, THREADS,
"NOTLS must free slots on txn drop; only {got}/{THREADS} live threads got one"
);
}
@@ -8,8 +8,9 @@
//! 3. The watcher's event handler detects the directory Create event,
//! collects it, and sends it to the owner thread via `watch_tx`.
//! 4. The owner thread adds a NonRecursive watch on the new directory and
//! does a flat (non-recursive) read_dir to inject files that already
//! exist (race-window coverage).
//! walks its subtree (`index_new_directory`) to inject files that
//! already exist (race-window + burst/mv-in coverage) and to watch
//! nested subdirectories.
//! 5. Files created *after* the watch is established are picked up via
//! normal event delivery.
//!
@@ -469,6 +470,160 @@ fn burst_file_creation_in_new_directory() {
}
}
/// bug pinning #725: a directory that already exists but is EMPTY at
/// initial scan time is absent from `sync_data.dirs` and missing watch events
#[test]
fn file_created_in_preexisting_empty_directory() {
let tmp = TempDir::new().unwrap();
let base = tmp.path().canonicalize().unwrap();
// `commands/` is empty during the initial scan — only `init.lua` is indexed.
fs::create_dir_all(base.join("commands")).unwrap();
fs::write(base.join("init.lua"), "-- init\n").unwrap();
let (shared_picker, _frecency) = make_watched_picker(&base);
wait_ready(&shared_picker);
// Now write a file into the directory that was empty at scan time.
fs::write(
base.join("commands/review.md"),
"# Review\nEMPTY_DIR_REVIEW_TOKEN\n",
)
.unwrap();
let elapsed = poll_until(
&shared_picker,
WATCHER_TIMEOUT,
"file commands/review.md created in a pre-existing empty directory",
|picker| {
picker
.get_files()
.iter()
.any(|f| f.relative_path(picker).contains("review.md"))
},
);
eprintln!(
" File in pre-existing empty directory detected in {:.0}ms",
elapsed.as_secs_f64() * 1000.0
);
}
/// Same as above but with a nested chain of empty directories under an
/// indexed one: every level of the empty subtree must be watched.
#[test]
fn file_created_in_nested_preexisting_empty_directories() {
let tmp = TempDir::new().unwrap();
let base = tmp.path().canonicalize().unwrap();
// `src/` is indexed (has a file); `src/plugins/extra/` is an empty chain.
fs::create_dir_all(base.join("src/plugins/extra")).unwrap();
fs::write(base.join("src/main.rs"), "fn main() {}\n").unwrap();
git_init_and_commit(&base);
let (shared_picker, _frecency) = make_watched_picker(&base);
wait_ready(&shared_picker);
fs::write(
base.join("src/plugins/extra/loader.rs"),
"pub fn load() {}\nconst TOKEN: &str = \"NESTED_EMPTY_DIR_TOKEN\";\n",
)
.unwrap();
let elapsed = poll_until(
&shared_picker,
WATCHER_TIMEOUT,
"file src/plugins/extra/loader.rs created in nested empty directories",
|picker| {
picker
.get_files()
.iter()
.any(|f| f.relative_path(picker).contains("loader.rs"))
},
);
eprintln!(
" File in nested empty directories detected in {:.0}ms",
elapsed.as_secs_f64() * 1000.0
);
poll_until(
&shared_picker,
WATCHER_TIMEOUT,
"grep finds NESTED_EMPTY_DIR_TOKEN",
|picker| grep_plain_count(picker, "NESTED_EMPTY_DIR_TOKEN") >= 1,
);
}
#[test]
fn nested_tree_created_in_one_burst_detected() {
let tmp = TempDir::new().unwrap();
let base = tmp.path().canonicalize().unwrap();
fs::write(base.join("root.txt"), "root file\n").unwrap();
git_init_and_commit(&base);
let (shared_picker, _frecency) = make_watched_picker(&base);
wait_ready(&shared_picker);
// No sleeps between levels: the watcher sees one Create for `pkg` and
// must index the whole subtree from it.
fs::create_dir_all(base.join("pkg/src/nested")).unwrap();
fs::write(base.join("pkg/Cargo.toml"), "[package]\n").unwrap();
fs::write(
base.join("pkg/src/lib.rs"),
"const TOKEN: &str = \"BURST_TREE_LIB_TOKEN\";\n",
)
.unwrap();
fs::write(
base.join("pkg/src/nested/deep.rs"),
"const TOKEN: &str = \"BURST_TREE_DEEP_TOKEN\";\n",
)
.unwrap();
for rel in ["pkg/Cargo.toml", "pkg/src/lib.rs", "pkg/src/nested/deep.rs"] {
let elapsed = poll_until(
&shared_picker,
WATCHER_TIMEOUT,
&format!("burst-created file {rel}"),
|picker| {
picker
.get_files()
.iter()
.any(|f| f.relative_path(picker) == rel)
},
);
eprintln!(
" Burst file {rel} detected in {:.0}ms",
elapsed.as_secs_f64() * 1000.0
);
}
// Files created later at the deepest level need the nested watches too.
fs::write(
base.join("pkg/src/nested/late.rs"),
"const TOKEN: &str = \"BURST_TREE_LATE_TOKEN\";\n",
)
.unwrap();
poll_until(
&shared_picker,
WATCHER_TIMEOUT,
"late file in burst-created nested dir",
|picker| {
picker
.get_files()
.iter()
.any(|f| f.relative_path(picker).ends_with("late.rs"))
},
);
poll_until(
&shared_picker,
WATCHER_TIMEOUT,
"grep finds BURST_TREE_DEEP_TOKEN",
|picker| grep_plain_count(picker, "BURST_TREE_DEEP_TOKEN") >= 1,
);
}
/// Verify that gitignored directories created at runtime are NOT watched
/// and their files do NOT appear in the index.
#[test]
@@ -222,6 +222,17 @@ fn multi_grep_with_file_path_suffix_constraint() {
}
}
#[test]
fn multi_grep_with_missing_file_path_constraint_returns_no_matches() {
let tmp = TempDir::new().unwrap();
let picker = create_picker(tmp.path(), &[("other.lua", "handleRequest\n")]);
let constraints = [Constraint::FilePath("missing.lua")];
let result = picker.multi_grep(&["handleRequest"], &constraints, &plain_opts());
assert!(result.matches.is_empty());
}
/// Glob constraints must match native Windows paths — the picker normalises
/// separators when handing paths to the glob matcher.
#[test]
+343
View File
@@ -0,0 +1,343 @@
#![cfg(rescan_stats)]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use fff_search::file_picker::{FFFMode, FilePicker};
use fff_search::{FilePickerOptions, RescanStats, SharedFilePicker, SharedFrecency};
use tempfile::TempDir;
const SETTLE: Duration = Duration::from_millis(600);
#[test]
fn saving_source_files_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\n");
for i in 0..20 {
write(base, &format!("src/mod{i}.rs"), "pub fn f() {}");
}
});
for round in 0..10 {
for i in 0..20 {
repo.write(
&format!("src/mod{i}.rs"),
&format!("pub fn f() {{ let _ = {round}; }}"),
);
}
repo.settle();
}
repo.assert_quiet("200 file saves");
}
#[test]
fn build_output_in_ignored_directories_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\nnode_modules/\ndist/\n");
write(base, "src/main.rs", "fn main() {}");
});
for round in 0..4 {
for i in 0..150 {
repo.write(&format!("target/debug/deps/unit-{round}-{i}.o"), "binary");
repo.write(&format!("dist/chunk-{round}-{i}.js"), "bundled");
}
repo.settle();
}
repo.assert_quiet("1200 build artifacts written into ignored directories");
}
#[test]
fn adding_source_files_and_directories_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\n");
write(base, "src/main.rs", "fn main() {}");
});
for i in 0..40 {
repo.write(&format!("src/feature{i}/mod.rs"), "pub mod inner;");
repo.write(&format!("src/feature{i}/inner.rs"), "pub fn go() {}");
}
repo.settle();
assert!(
repo.wait_indexed("src/feature39/inner.rs"),
"watcher must index files in newly created directories"
);
repo.assert_quiet("40 new directories with 80 files");
}
#[test]
fn recreating_generated_files_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\n");
write(base, "src/main.rs", "fn main() {}");
});
// Recreated paths must reuse their overflow slots.
for round in 0..12 {
for i in 0..40 {
repo.write(&format!("src/generated/api{i}.rs"), "pub struct A;");
}
repo.settle();
for i in 0..40 {
repo.remove(&format!("src/generated/api{i}.rs"));
}
repo.settle();
assert!(
repo.overflow_len() <= 64,
"round {round}: regenerating the same paths grew the overflow region to {}",
repo.overflow_len()
);
}
repo.assert_quiet("12 codegen cycles over 40 stable paths");
}
#[test]
fn git_workflow_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\n");
write(base, "src/main.rs", "fn main() {}");
write(base, "src/lib.rs", "pub mod thing;");
git(base, &["init", "-b", "main"]);
git(base, &["add", "-A"]);
git(base, &["commit", "-m", "initial"]);
});
repo.write("src/main.rs", "fn main() { println!(\"hi\"); }");
repo.settle();
repo.git(&["add", "-A"]);
repo.settle();
repo.git(&["commit", "-m", "second"]);
repo.settle();
repo.git(&["checkout", "-b", "feature"]);
repo.settle();
repo.write("src/feature.rs", "pub fn feature() {}");
repo.git(&["add", "-A"]);
repo.git(&["commit", "-m", "feature"]);
repo.settle();
repo.git(&["checkout", "main"]);
repo.settle();
repo.git(&["merge", "feature"]);
repo.settle();
repo.assert_quiet("a commit / branch / merge cycle");
}
#[test]
fn reading_files_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "target/\n");
for i in 0..50 {
write(base, &format!("src/mod{i}.rs"), "pub fn f() {}");
}
});
// Preview rendering and grep open every file in the result list. Reacting
// to those reads would make the picker rescan while the user scrolls.
for _ in 0..5 {
for i in 0..50 {
let _ = std::fs::read(repo.path(&format!("src/mod{i}.rs"))).unwrap();
}
}
repo.settle();
repo.assert_quiet("reading every indexed file");
}
#[test]
fn npm_install_style_churn_does_not_rescan() {
let repo = WatchedRepo::new(|base| {
write(base, ".gitignore", "node_modules/\n");
write(base, "src/index.ts", "export const a = 1;");
});
for pkg in 0..100 {
repo.write(&format!("node_modules/pkg{pkg}/package.json"), "{}");
repo.write(
&format!("node_modules/pkg{pkg}/index.js"),
"module.exports={}",
);
repo.write(&format!("node_modules/pkg{pkg}/.gitignore"), "dist\n");
}
repo.settle();
repo.settle();
repo.assert_quiet("an npm install into an ignored node_modules");
}
#[test]
fn a_churning_root_is_capped_at_one_rescan_per_cooldown() {
let repo = WatchedRepo::new(|base| {
write(base, "src/main.rs", "fn main() {}");
});
// Root ignore changes force watcher rescan requests.
for round in 0..25 {
repo.write(".gitignore", &format!("target/\n# round {round}\n"));
std::thread::sleep(Duration::from_millis(120));
}
repo.settle();
let stats = repo.rescans();
assert!(
stats.total <= 1,
"a churning root must not exceed one walk per cooldown, got {stats}"
);
assert!(
stats.throttled > 0,
"the suppressed triggers must be recorded, got {stats}"
);
}
struct WatchedRepo {
base: PathBuf,
picker: SharedFilePicker,
_frecency: SharedFrecency,
_tmp: TempDir,
}
impl WatchedRepo {
fn new(setup: impl FnOnce(&Path)) -> Self {
let tmp = tempfile::tempdir().unwrap();
let base = fff_search::path_utils::canonicalize(tmp.path()).unwrap();
setup(&base);
let picker = SharedFilePicker::default();
let frecency = SharedFrecency::noop();
FilePicker::new_with_shared_state(
picker.clone(),
frecency.clone(),
FilePickerOptions {
base_path: base.to_string_lossy().into_owned(),
enable_mmap_cache: false,
mode: FFFMode::Neovim,
watch: true,
..Default::default()
},
)
.expect("failed to create file picker");
assert!(
picker.wait_for_scan(Duration::from_secs(60)),
"timed out waiting for the initial scan"
);
assert!(
picker.wait_for_watcher(Duration::from_secs(60)),
"timed out waiting for the watcher"
);
let repo = Self {
base,
picker,
_frecency: frecency,
_tmp: tmp,
};
repo.settle();
repo.picker.reset_rescan_stats();
repo
}
fn settle(&self) {
std::thread::sleep(SETTLE);
assert!(
self.picker
.wait_for_indexing_complete(Duration::from_secs(60)),
"timed out waiting for background indexing to finish"
);
}
fn assert_quiet(&self, workload: &str) {
let stats = self.rescans();
assert_eq!(
stats.watcher_triggered(),
0,
"{workload} must be absorbed incrementally, but the watcher fell back to {stats}"
);
}
fn rescans(&self) -> RescanStats {
self.picker.rescan_stats()
}
fn path(&self, rel: &str) -> PathBuf {
self.base.join(rel)
}
fn write(&self, rel: &str, contents: &str) {
write(&self.base, rel, contents);
}
fn remove(&self, rel: &str) {
std::fs::remove_file(self.path(rel)).unwrap();
}
fn git(&self, args: &[&str]) {
git(&self.base, args);
}
fn wait_indexed(&self, rel: &str) -> bool {
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while std::time::Instant::now() < deadline {
if self.is_indexed(rel) {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
false
}
fn is_indexed(&self, rel: &str) -> bool {
let guard = self.picker.read().unwrap();
guard
.as_ref()
.and_then(|p| p.get_file_by_path(self.path(rel)))
.is_some_and(|file| !file.is_deleted())
}
fn overflow_len(&self) -> usize {
let guard = self.picker.read().unwrap();
guard
.as_ref()
.map(|p| p.get_overflow_files().len())
.unwrap_or(0)
}
}
impl Drop for WatchedRepo {
fn drop(&mut self) {
// Stop the watcher before the tree disappears, otherwise a late batch
// races the tempdir removal.
if let Ok(mut guard) = self.picker.write() {
guard.take();
}
}
}
fn write(base: &Path, rel: &str, contents: &str) {
let path = base.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn git(dir: &Path, args: &[&str]) {
let out = Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap_or_else(|e| panic!("git {args:?} failed to spawn: {e}"));
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
+1 -1
View File
@@ -3,7 +3,7 @@ name = "fff-grep"
description = "File grepping logic for fff"
license = "MIT"
authors = ["Dmitriy Kovalenko <dmtr.kovalenko@outlok.com>"]
version = "0.10.0"
version = "0.10.5"
edition = "2024"
[lints]
+13 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-mcp"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
description = "MCP server for FFF file finder - drop-in replacement for AI code assistant search tools"
license = "MIT"
@@ -19,8 +19,8 @@ ripgrep = ["fff/ripgrep", "fff-query-parser/ripgrep"]
zlob = ["fff/zlob", "fff-query-parser/zlob"]
[dependencies]
fff = { package = "fff-search", path = "../fff-core", default-features = false , version = "0.10.0", features = ["definitions"] }
fff-query-parser = { path = "../fff-query-parser", default-features = false , version = "0.10.0" }
fff = { package = "fff-search", path = "../fff-core", default-features = false , version = "0.10.5", features = ["definitions"] }
fff-query-parser = { path = "../fff-query-parser", default-features = false , version = "0.10.5" }
mimalloc = { workspace = true }
rmcp = { version = "1.7.0", features = ["server", "transport-io"] }
schemars = "1.0"
@@ -30,3 +30,13 @@ tokio = { version = "1", features = ["full"] }
tracing = { workspace = true }
git2 = { workspace = true }
clap = { version = "4", features = ["derive", "env"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60", features = [
"Win32_Foundation",
"Win32_System_Threading",
"Win32_System_Diagnostics_ToolHelp",
] }
[dev-dependencies]
tempfile = "3.8"
+184
View File
@@ -0,0 +1,184 @@
use rmcp::model::{ClientRequest, ErrorCode, ErrorData, JsonRpcMessage};
use rmcp::service::{RoleServer, RxJsonRpcMessage, TxJsonRpcMessage};
use rmcp::transport::Transport;
/// rmcp aborts startup on any pre-`initialize` request except `ping`, which kills the process
/// before the client can retry. Answer such probes with `-32601` and keep waiting for the legacy
/// handshake, so clients speaking the stateless spec (`server/discover`, SEP-1442) can fall back.
/// @see https://github.com/dmtrKovalenko/fff/issues/797
pub(crate) struct ProbeTolerantTransport<T> {
inner: T,
initialized: bool,
}
impl<T> ProbeTolerantTransport<T> {
pub(crate) fn new(inner: T) -> Self {
Self {
inner,
initialized: false,
}
}
}
impl<T> Transport<RoleServer> for ProbeTolerantTransport<T>
where
T: Transport<RoleServer>,
{
type Error = T::Error;
fn send(
&mut self,
item: TxJsonRpcMessage<RoleServer>,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
self.inner.send(item)
}
async fn receive(&mut self) -> Option<RxJsonRpcMessage<RoleServer>> {
loop {
let msg = self.inner.receive().await?;
if self.initialized {
return Some(msg);
}
let JsonRpcMessage::Request(request) = &msg else {
return Some(msg);
};
match &request.request {
ClientRequest::InitializeRequest(_) => {
self.initialized = true;
return Some(msg);
}
// rmcp answers pre-init pings itself
ClientRequest::PingRequest(_) => return Some(msg),
unsupported => {
let error = unsupported_probe_error(unsupported.method());
tracing::warn!(
method = unsupported.method(),
"rejecting pre-initialize request, awaiting initialize"
);
let id = request.id.clone();
self.inner
.send(JsonRpcMessage::error(error, Some(id)))
.await
.ok()?;
}
}
}
}
fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
self.inner.close()
}
}
fn unsupported_probe_error(method: &str) -> ErrorData {
ErrorData::new(
ErrorCode::METHOD_NOT_FOUND,
format!(
"{method} is not supported before initialize; this server uses the initialize handshake"
),
None,
)
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::{
ClientCapabilities, ClientJsonRpcMessage, CustomRequest, Implementation, InitializeRequest,
InitializeRequestParams, NumberOrString, ServerJsonRpcMessage,
};
use std::collections::VecDeque;
#[derive(Default)]
struct MockTransport {
incoming: VecDeque<ClientJsonRpcMessage>,
sent: Vec<ServerJsonRpcMessage>,
}
impl Transport<RoleServer> for MockTransport {
type Error = std::io::Error;
fn send(
&mut self,
item: ServerJsonRpcMessage,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
self.sent.push(item);
async { Ok(()) }
}
fn receive(&mut self) -> impl Future<Output = Option<ClientJsonRpcMessage>> + Send {
let next = self.incoming.pop_front();
async move { next }
}
async fn close(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
fn custom_request(id: i64, method: &str) -> ClientJsonRpcMessage {
ClientJsonRpcMessage::request(
ClientRequest::CustomRequest(CustomRequest::new(method, Some(serde_json::json!({})))),
NumberOrString::Number(id),
)
}
fn initialize_request(id: i64) -> ClientJsonRpcMessage {
ClientJsonRpcMessage::request(
ClientRequest::InitializeRequest(InitializeRequest::new(InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new("probe", "1"),
))),
NumberOrString::Number(id),
)
}
#[tokio::test]
async fn pre_init_probe_is_rejected_and_initialize_still_arrives() {
let inner = MockTransport {
incoming: VecDeque::from(vec![
custom_request(1, "server/discover"),
initialize_request(2),
]),
sent: Vec::new(),
};
let mut transport = ProbeTolerantTransport::new(inner);
let received = transport.receive().await.expect("initialize forwarded");
assert!(matches!(
received,
JsonRpcMessage::Request(req)
if matches!(req.request, ClientRequest::InitializeRequest(_))
));
let JsonRpcMessage::Error(err) = &transport.inner.sent[0] else {
panic!("expected an error response for the probe");
};
assert_eq!(err.id, Some(NumberOrString::Number(1)));
assert_eq!(err.error.code, ErrorCode::METHOD_NOT_FOUND);
assert!(err.error.message.contains("server/discover"));
}
#[tokio::test]
async fn post_init_requests_pass_through_untouched() {
let inner = MockTransport {
incoming: VecDeque::from(vec![
initialize_request(1),
custom_request(2, "server/discover"),
]),
sent: Vec::new(),
};
let mut transport = ProbeTolerantTransport::new(inner);
transport.receive().await.expect("initialize forwarded");
let received = transport.receive().await.expect("custom request forwarded");
assert!(matches!(
received,
JsonRpcMessage::Request(req)
if matches!(req.request, ClientRequest::CustomRequest(_))
));
assert!(transport.inner.sent.is_empty());
}
}
+92 -15
View File
@@ -1,16 +1,22 @@
mod cursor;
mod handshake;
mod healthcheck;
mod output;
mod parent;
mod server;
mod update_check;
use std::time::{Duration, SystemTime};
use clap::Parser;
use fff::file_picker::FilePicker;
use fff::frecency::FrecencyTracker;
use fff::{FFFMode, SharedFilePicker, SharedFrecency};
use git2::Repository;
use handshake::ProbeTolerantTransport;
use mimalloc::MiMalloc;
use rmcp::{ServiceExt, transport::stdio};
use rmcp::ServiceExt;
use rmcp::transport::async_rw::AsyncRwTransport;
use server::FffServer;
#[global_allocator]
@@ -92,7 +98,7 @@ pub const MCP_INSTRUCTIONS: &str = concat!(
" !generated/ - exclude generated code",
);
/// FFF MCP Server -- a high performance & accuracy file finder for AI code assistants.
/// FFF MCP Server - a high performance & accuracy file finder for AI code assistants.
#[derive(Parser)]
#[command(name = "fff-mcp", version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("FFF_GIT_HASH"), ")"))]
pub(crate) struct Args {
@@ -131,6 +137,7 @@ pub(crate) struct Args {
/// Disable the content index built after the initial scan.
/// This makes grep calls slower but consumes less RAM (recommended to not turn off)
#[arg(long = "no-content-indexing")]
no_content_indexing: bool,
/// Explicitly enable content indexing even when `--no-warmup` is set.
@@ -154,15 +161,40 @@ pub(crate) struct Args {
#[arg(long = "follow-symlinks")]
follow_symlinks: bool,
/// Allow indexing the user's home directory. FFF refuses to init in `~`
/// unless this is set. Also settable via FFF_ENABLE_HOME_SCAN=1.
#[arg(
long = "enable-home-scan",
env = "FFF_ENABLE_HOME_SCAN",
num_args = 0..=1,
default_missing_value = "true",
default_value_t = false,
value_parser = clap::builder::BoolishValueParser::new()
)]
enable_home_scan: bool,
/// Allow indexing the filesystem root, off by default for the same reason.
/// Also settable via FFF_ENABLE_ROOT_SCAN=1.
#[arg(
long = "enable-root-scan",
env = "FFF_ENABLE_ROOT_SCAN",
num_args = 0..=1,
default_missing_value = "true",
default_value_t = false,
value_parser = clap::builder::BoolishValueParser::new()
)]
enable_root_scan: bool,
/// Run a health check and print diagnostic information, then exit.
#[arg(long = "healthcheck")]
pub(crate) healthcheck: bool,
/// Exit after this many seconds of inactivity. 0 = never exit.
/// Timeout of inactivity after which fff mcp will be exited. Even if the parent process
/// is alive we don't want to occupy resources on index and file watches if fff is unused
#[arg(
long = "idle-timeout-secs",
env = "FFF_MCP_IDLE_TIMEOUT_SECS",
default_value_t = 900
default_value_t = 60 * 60
)]
idle_timeout_secs: u64,
}
@@ -276,7 +308,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.max_cached_files
.map(fff::ContentCacheBudget::new_for_repo),
follow_symlinks: args.follow_symlinks,
..Default::default()
enable_home_dir_scanning: args.enable_home_scan,
enable_fs_root_scanning: args.enable_root_scan,
},
)
.map_err(|e| format!("Failed to init file picker: {}", e))?;
@@ -310,16 +343,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
const STARTUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
let service = match tokio::time::timeout(STARTUP_TIMEOUT, server.serve(stdio())).await {
let stdio = AsyncRwTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
let transport = ProbeTolerantTransport::new(stdio);
let service = match tokio::time::timeout(STARTUP_TIMEOUT, server.serve(transport)).await {
Ok(res) => res.map_err(|e| format!("Failed to start MCP server: {}", e))?,
Err(_) => {
return Err("MCP initialize handshake did not complete within 60s".into());
}
};
if idle_timeout_secs > 0 {
let parent_watcher = parent::ParentWatcher::new();
match &parent_watcher {
Some(watcher) => tracing::info!(
"Watching parent process (pid {}); will exit when it dies",
watcher.parent_pid()
),
None => tracing::warn!(
"Parent process liveness detection unavailable; idle timeout will exit unconditionally"
),
}
if idle_timeout_secs > 0 || parent_watcher.is_some() {
last_activity.store(
std::time::SystemTime::now()
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
@@ -328,9 +374,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let last_activity_for_watchdog = last_activity.clone();
tokio::spawn(async move {
let tick = std::time::Duration::from_secs(60);
let tick = watchdog_interval();
loop {
tokio::time::sleep(tick).await;
if let Some(ref watcher) = parent_watcher {
if !watcher.parent_alive() {
tracing::info!(
"Parent process (pid {}) exited, shutting down",
watcher.parent_pid()
);
flush_logs_and_exit().await;
}
// Parent is alive: it owns our lifecycle, never exit on idle
// Clients like Codex do not restart MCP servers @see #703
continue;
}
if idle_timeout_secs == 0 {
continue;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
@@ -338,12 +402,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let last = last_activity_for_watchdog.load(std::sync::atomic::Ordering::Relaxed);
if now.saturating_sub(last) >= idle_timeout_secs {
tracing::info!(
"Exiting after {}s of inactivity (idle_timeout_secs={})",
now.saturating_sub(last),
idle_timeout_secs
);
std::process::exit(0);
tracing::info!(?idle_timeout_secs, "Exiting due to inactivity",);
flush_logs_and_exit().await;
}
}
});
@@ -370,3 +430,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
// Tracing appender is non blocking, to get full log give it some time before hard exit
async fn flush_logs_and_exit() -> ! {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
std::process::exit(0);
}
fn watchdog_interval() -> Duration {
if cfg!(debug_assertions)
&& let Some(milliseconds) = std::env::var("FFF_MCP_TEST_WATCHDOG_INTERVAL_MS")
.ok()
.and_then(|value| value.parse().ok())
{
return Duration::from_millis(milliseconds);
}
Duration::from_secs(60)
}
+99
View File
@@ -0,0 +1,99 @@
#[cfg(unix)]
mod imp {
pub struct ParentWatcher {
ppid: u32,
}
impl ParentWatcher {
pub fn new() -> Option<Self> {
let ppid = std::os::unix::process::parent_id();
// ppid <= 1 means we were spawned by init and can't detect death
(ppid > 1).then_some(Self { ppid })
}
pub fn parent_pid(&self) -> u32 {
self.ppid
}
// When the parent dies the kernel reparents us, so getppid() changes.
// Race-free and immune to PID reuse, unlike kill(ppid, 0).
pub fn parent_alive(&self) -> bool {
std::os::unix::process::parent_id() == self.ppid
}
}
}
#[cfg(windows)]
mod imp {
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_TIMEOUT};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcessId, OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};
pub struct ParentWatcher {
handle: HANDLE,
ppid: u32,
}
// HANDLE is a raw pointer; it is only ever used via WaitForSingleObject
// which is thread-safe, so moving/sharing the watcher across threads is fine.
unsafe impl Send for ParentWatcher {}
unsafe impl Sync for ParentWatcher {}
impl ParentWatcher {
pub fn new() -> Option<Self> {
let ppid = parent_pid_of_current()?;
let handle = unsafe { OpenProcess(PROCESS_SYNCHRONIZE, 0, ppid) };
if handle.is_null() {
return None;
}
// Holding the handle pins the PID, preventing reuse for the process lifetime
Some(Self { handle, ppid })
}
pub fn parent_pid(&self) -> u32 {
self.ppid
}
pub fn parent_alive(&self) -> bool {
unsafe { WaitForSingleObject(self.handle, 0) == WAIT_TIMEOUT }
}
}
impl Drop for ParentWatcher {
fn drop(&mut self) {
unsafe { CloseHandle(self.handle) };
}
}
fn parent_pid_of_current() -> Option<u32> {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == INVALID_HANDLE_VALUE {
return None;
}
let mut entry: PROCESSENTRY32 = std::mem::zeroed();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
let current = GetCurrentProcessId();
let mut found = None;
if Process32First(snapshot, &mut entry) != 0 {
loop {
if entry.th32ProcessID == current {
found = Some(entry.th32ParentProcessID);
break;
}
if Process32Next(snapshot, &mut entry) == 0 {
break;
}
}
}
CloseHandle(snapshot);
found
}
}
}
pub use imp::ParentWatcher;
+21 -56
View File
@@ -7,7 +7,6 @@ use fff_query_parser::AiGrepConfig;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::*;
use rmcp::{ServerHandler, schemars, tool, tool_handler, tool_router};
use std::borrow::Cow;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -421,7 +420,12 @@ impl FffServer {
/// IMPORTANT: Keep queries SHORT — prefer 1-2 terms max.
#[tool(
name = "find_files",
description = "Fuzzy file search by name. Searches FILE NAMES, not file contents. Use it when you need to find a file, not a definition. Use grep instead for searching code content (definitions, usage patterns). Supports fuzzy matching, path prefixes ('src/'), and glob constraints ('name **/src/*.{ts,tsx} !test/'). IMPORTANT: Keep queries SHORT — prefer 1-2 terms max. Multiple words are a waterfall (each narrows results), NOT OR. If unsure, start broad with 1 term and refine."
description = "Fuzzy file search by name. Searches FILE NAMES, not file contents. Use it when you need to find a file, not a definition. Use grep instead for searching code content (definitions, usage patterns). Supports fuzzy matching, path prefixes ('src/'), and glob constraints ('name **/src/*.{ts,tsx} !test/'). IMPORTANT: Keep queries SHORT — prefer 1-2 terms max. Multiple words are a waterfall (each narrows results), NOT OR. If unsure, start broad with 1 term and refine.",
annotations(
read_only_hint = true,
destructive_hint = false,
open_world_hint = false
)
)]
fn find_files(
&self,
@@ -536,7 +540,12 @@ impl FffServer {
/// Prefer plain text over regex. Filter files with constraints.
#[tool(
name = "grep",
description = "Search file contents. Search for bare identifiers (e.g. 'InProgressQuote', 'ActorAuth'), NOT code syntax or regex. Filter files with constraints (e.g. '*.rs query', 'src/ query'). Use filename, directory (ending with /) or glob expressions to prefilter. See server instructions for constraint syntax and core rules."
description = "Search file contents. Search for bare identifiers (e.g. 'InProgressQuote', 'ActorAuth'), NOT code syntax or regex. Filter files with constraints (e.g. '*.rs query', 'src/ query'). Use filename, directory (ending with /) or glob expressions to prefilter. See server instructions for constraint syntax and core rules.",
annotations(
read_only_hint = true,
destructive_hint = false,
open_world_hint = false
)
)]
fn grep(
&self,
@@ -573,7 +582,12 @@ impl FffServer {
/// Patterns are literal text — NEVER escape special characters.
#[tool(
name = "multi_grep",
description = "Search file contents for lines matching ANY of multiple patterns (OR logic). IMPORTANT: This returns files where ANY query matches, NOT all patterns. Patterns are literal text — NEVER escape special characters (no \\( \\) \\. etc). Faster than regex alternation for literal text. See server instructions for constraint syntax."
description = "Search file contents for lines matching ANY of multiple patterns (OR logic). IMPORTANT: This returns files where ANY query matches, NOT all patterns. Patterns are literal text — NEVER escape special characters (no \\( \\) \\. etc). Faster than regex alternation for literal text. See server instructions for constraint syntax.",
annotations(
read_only_hint = true,
destructive_hint = false,
open_world_hint = false
)
)]
fn multi_grep(
&self,
@@ -613,61 +627,12 @@ impl FffServer {
.ok_or_else(|| ErrorData::internal_error("File picker not initialized", None))?;
let patterns_refs: Vec<&str> = params.patterns.iter().map(|s| s.as_str()).collect();
let parser = fff_query_parser::QueryParser::new(fff_query_parser::AiGrepConfig);
let parsed_constraints = parser.parse(constraint_query);
let constraints = parsed_constraints.constraints.as_slice();
let parser = QueryParser::new(AiGrepConfig);
let constraints = parser.parse_constraints(constraint_query);
let result = picker.multi_grep(&patterns_refs, constraints, &options);
let result = picker.multi_grep(&patterns_refs, &constraints, &options);
let file_refs: Vec<&FileItem> = result.files.to_vec();
if result.matches.is_empty() && file_offset == 0 {
// Fallback: try individual patterns with plain grep
let (fallback_options, _) =
make_grep_options(output_mode, GrepMode::PlainText, 0, context);
let fallback_options = GrepSearchOptions {
time_budget_ms: 3000,
before_context: 0,
..fallback_options
};
for pat in &params.patterns {
let full_query: Cow<str> = if !constraint_query.is_empty() {
Cow::Owned(format!("{} {}", constraint_query, pat))
} else {
Cow::Borrowed(pat)
};
let parsed = parser.parse(&full_query);
let fb_result = picker.grep(&parsed, &fallback_options);
if !fb_result.matches.is_empty() {
let fb_file_refs: Vec<&FileItem> = fb_result.files.to_vec();
let mut cs = self.lock_cursors()?;
let text = &GrepFormatter {
matches: &fb_result.matches,
files: &fb_file_refs,
total_matched: fb_result.matches.len(),
next_file_offset: fb_result.next_file_offset,
output_mode,
max_results,
show_context: false,
auto_expand_defs: auto_expand,
picker,
}
.format(&mut cs);
return Ok(CallToolResult::success(vec![Content::text(format!(
"0 multi-pattern matches. Plain grep fallback for \"{}\":\n{}",
pat, text
))]));
}
}
return Ok(CallToolResult::success(vec![Content::text(
"0 matches.".to_string(),
)]));
}
if result.matches.is_empty() {
return Ok(CallToolResult::success(vec![Content::text(
"0 matches.".to_string(),
+50 -28
View File
@@ -1,20 +1,14 @@
//! Background update checker — compares the embedded build hash against
//! the latest GitHub release tag to surface upgrade notices in MCP instructions.
use std::sync::OnceLock;
const REPO: &str = "dmtrKovalenko/fff.nvim";
const BUILD_HASH: &str = env!("FFF_GIT_HASH");
const REPO: &str = "dmtrKovalenko/fff";
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Holds the result of the update check (empty string = up to date or check failed).
static UPDATE_NOTICE: OnceLock<String> = OnceLock::new();
/// Returns the update notice if the check has completed, empty string otherwise.
pub fn get_update_notice() -> &'static str {
UPDATE_NOTICE.get().map(|s| s.as_str()).unwrap_or("")
}
/// Kick off the update check in a background thread so it never blocks the server.
pub fn spawn_update_check() {
std::thread::spawn(|| {
let notice = check_latest_release();
@@ -22,34 +16,28 @@ pub fn spawn_update_check() {
});
}
/// Fetch the latest release tag from GitHub and compare against the build hash.
fn check_latest_release() -> String {
match fetch_latest_tag() {
Ok(tag) => compare_versions(BUILD_HASH, &tag),
match fetch_latest_stable_tag() {
Ok(tag) => compare_versions(CURRENT_VERSION, &tag),
Err(_) => String::new(),
}
}
/// Compare a build hash against a release tag.
/// Returns an update notice string, or empty if up-to-date.
fn compare_versions(build_hash: &str, release_tag: &str) -> String {
fn compare_versions(current_version: &str, release_tag: &str) -> String {
let tag = release_tag.trim();
if tag.is_empty() || build_hash == "unknown" {
return String::new();
}
let our_short = &build_hash[..build_hash.len().min(tag.len())];
if our_short == tag {
let tag_version = tag.strip_prefix('v').unwrap_or(tag);
if tag.is_empty() || tag_version == current_version {
return String::new();
}
format!(
"\n[fff update available: `curl -fsSL https://raw.githubusercontent.com/{REPO}/main/install-mcp.sh | bash`]\n"
"\n[fff update available ({current_version} -> {tag_version}): `curl -fsSL https://raw.githubusercontent.com/{REPO}/main/install-mcp.sh | bash`]\n"
)
}
/// Shell out to curl to fetch the latest release tag name from GitHub API.
fn fetch_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
// Uses /releases/latest — GitHub excludes prereleases here, matching the
// stable channel that install-mcp.sh installs from.
fn fetch_latest_stable_tag() -> Result<String, Box<dyn std::error::Error>> {
let output = std::process::Command::new("curl")
.args([
"-fsSL",
@@ -57,7 +45,7 @@ fn fetch_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
"5",
"-H",
"Accept: application/vnd.github.v3+json",
&format!("https://api.github.com/repos/{REPO}/releases?per_page=1"),
&format!("https://api.github.com/repos/{REPO}/releases/latest"),
])
.output()?;
@@ -66,13 +54,47 @@ fn fetch_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
}
let body = String::from_utf8(output.stdout)?;
let releases: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let tag = releases
.first()
.and_then(|r| r.get("tag_name"))
let release: serde_json::Value = serde_json::from_str(&body)?;
let tag = release
.get("tag_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Ok(tag)
}
#[cfg(test)]
mod tests {
use super::compare_versions;
#[test]
fn same_version_with_v_prefix_is_silent() {
assert_eq!(compare_versions("0.10.1", "v0.10.1"), "");
}
#[test]
fn same_version_without_v_prefix_is_silent() {
assert_eq!(compare_versions("0.10.1", "0.10.1"), "");
}
#[test]
fn empty_tag_is_silent() {
assert_eq!(compare_versions("0.10.1", ""), "");
assert_eq!(compare_versions("0.10.1", " "), "");
}
#[test]
fn older_current_reports_update() {
let notice = compare_versions("0.10.0", "v0.10.1");
assert!(notice.contains("0.10.0 -> 0.10.1"), "got: {notice}");
assert!(notice.contains("install-mcp.sh"));
}
#[test]
fn nightly_tag_never_equals_stable_current() {
let notice = compare_versions("0.10.1", "0.10.2-nightly.6a239e9");
assert!(!notice.is_empty());
assert!(notice.contains("0.10.1 -> 0.10.2-nightly.6a239e9"));
}
}
+181
View File
@@ -0,0 +1,181 @@
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
const BIN: &str = env!("CARGO_BIN_EXE_fff-mcp");
#[test]
fn stays_alive_while_parent_alive_despite_idle_timeout() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
let mut child = Command::new(BIN)
.arg(dir.path())
.args([
"--no-update-check",
"--no-warmup",
"--no-watch",
"--idle-timeout-secs",
"1",
])
.arg("--log-file")
.arg(dir.path().join("test.log"))
.env("FFF_MCP_TEST_WATCHDOG_INTERVAL_MS", "100")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let stdout_lines = spawn_line_reader(child.stdout.take().unwrap());
do_handshake(&mut stdin, &stdout_lines);
// Wait past the idle timeout and several watchdog ticks.
std::thread::sleep(Duration::from_secs(2));
assert!(
child.try_wait().unwrap().is_none(),
"fff-mcp exited on idle timeout even though its parent is alive"
);
// Closing stdin ends the transport; the server must still shut down cleanly.
drop(stdin);
wait_for_exit(&mut child, Duration::from_secs(15));
}
#[cfg(unix)]
#[test]
fn exits_when_parent_dies_even_without_idle_timeout() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
let log_path = dir.path().join("test.log");
let exit_signal = dir.path().join("exit-parent");
// Intermediary parent: sh backgrounds fff-mcp and waits until the handshake
// completes before dying and orphaning it.
let mut sh = Command::new("sh")
.arg("-c")
.arg(
// Preserve stdin before POSIX shells assign /dev/null to background jobs.
r#"exec 3<&0
"$1" "$2" --no-update-check --no-warmup --no-watch \
--idle-timeout-secs 0 --log-file "$3" <&3 &
while [ ! -e "$4" ]; do sleep 0.1; done"#,
)
.arg("sh")
.arg(BIN)
.arg(dir.path())
.arg(&log_path)
.arg(&exit_signal)
.env("FFF_MCP_TEST_WATCHDOG_INTERVAL_MS", "100")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.unwrap();
let mut stdin = sh.stdin.take().unwrap();
let stdout_lines = spawn_line_reader(sh.stdout.take().unwrap());
do_handshake(&mut stdin, &stdout_lines);
std::fs::write(exit_signal, "").unwrap();
sh.wait().unwrap();
// We still hold the stdin write end, so the only exit path is the parent
// liveness check. EOF on stdout means fff-mcp closed it by exiting.
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match stdout_lines.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
Ok(_) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {
panic!("fff-mcp did not exit within 5s of its parent dying")
}
}
}
drop(stdin);
let logs = read_session_logs(dir.path());
assert!(
logs.contains("Parent process") && logs.contains("exited, shutting down"),
"expected parent-death exit reason in logs, got:\n{}",
logs
);
}
fn do_handshake(stdin: &mut ChildStdin, stdout_lines: &mpsc::Receiver<String>) {
let initialize = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "parent-liveness-test", "version": "0.0.0" }
}
});
writeln!(stdin, "{}", initialize).unwrap();
stdin.flush().unwrap();
let response = stdout_lines
.recv_timeout(Duration::from_secs(30))
.expect("no initialize response within 30s");
assert!(
response.contains("\"serverInfo\""),
"unexpected initialize response: {}",
response
);
writeln!(
stdin,
"{}",
serde_json::json!({ "jsonrpc": "2.0", "method": "notifications/initialized" })
)
.unwrap();
stdin.flush().unwrap();
}
fn spawn_line_reader(stdout: std::process::ChildStdout) -> mpsc::Receiver<String> {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
match line {
Ok(line) => {
if tx.send(line).is_err() {
break;
}
}
Err(_) => break,
}
}
});
rx
}
fn wait_for_exit(child: &mut Child, timeout: Duration) {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if child.try_wait().unwrap().is_some() {
return;
}
std::thread::sleep(Duration::from_millis(100));
}
child.kill().ok();
panic!(
"fff-mcp did not exit within {:?} after stdin closed",
timeout
);
}
#[cfg(unix)]
fn read_session_logs(dir: &std::path::Path) -> String {
let mut combined = String::new();
for entry in std::fs::read_dir(dir).unwrap().flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("test") && name.ends_with(".log") {
combined.push_str(&std::fs::read_to_string(entry.path()).unwrap_or_default());
}
}
combined
}
+5 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-nvim"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
[lints]
@@ -15,6 +15,8 @@ crate-type = ["cdylib", "rlib"]
default = ["ripgrep"]
ripgrep = ["fff/ripgrep", "fff-query-parser/ripgrep", "dep:ignore"]
zlob = ["fff/zlob", "fff-query-parser/zlob", "dep:zlob"]
# Keep full-rescan accounting in a release build; required by rescan_probe.
rescan-stats = ["fff/rescan-stats"]
[dependencies]
# Workspace dependencies
@@ -22,10 +24,10 @@ ahash = { workspace = true }
tracing = { workspace = true }
# Local crates
fff = { package = "fff-search", path = "../fff-core", version = "0.10.0", default-features = false, features = [
fff = { package = "fff-search", path = "../fff-core", version = "0.10.5", default-features = false, features = [
"mimalloc-collect",
] }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.0", default-features = false }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.5", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
ctrlc = "3.4.2"
git2 = { workspace = true }
+158
View File
@@ -0,0 +1,158 @@
use fff::file_picker::FilePicker;
use fff::{
FFFMode, FilePickerOptions, RESCAN_STATS_ENABLED, RescanReason, RescanStats, SharedFilePicker,
SharedFrecency,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
const POLL: Duration = Duration::from_millis(250);
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (base_path, run_for) = parse_args()?;
if !RESCAN_STATS_ENABLED {
return Err(
"this build has rescan accounting compiled out; rebuild with \
`--features rescan-stats` (or drop `--release`)"
.into(),
);
}
let picker = SharedFilePicker::default();
let frecency = SharedFrecency::noop();
println!("indexing {base_path} ...");
let started = Instant::now();
FilePicker::new_with_shared_state(
picker.clone(),
frecency.clone(),
FilePickerOptions {
base_path: base_path.clone(),
enable_mmap_cache: false,
mode: FFFMode::default(),
watch: true,
..Default::default()
},
)?;
if !picker.wait_for_scan(Duration::from_secs(600)) {
return Err("timed out waiting for the initial scan".into());
}
if !picker.wait_for_watcher(Duration::from_secs(600)) {
return Err("timed out waiting for the watcher".into());
}
println!(
"indexed {} files in {:.2}s; watching for rescan requests.\n",
live_files(&picker),
started.elapsed().as_secs_f64()
);
picker.reset_rescan_stats();
let running = Arc::new(AtomicBool::new(true));
let stop = Arc::clone(&running);
ctrlc::set_handler(move || stop.store(false, Ordering::SeqCst))?;
let watching_since = Instant::now();
let mut last = RescanStats::default();
while running.load(Ordering::SeqCst) {
std::thread::sleep(POLL);
let stats = picker.rescan_stats();
let delta = stats.since(&last);
if delta.total > 0 || delta.throttled > 0 {
let now = watching_since.elapsed().as_secs_f64();
let files = live_files(&picker);
let overflow = overflow_files(&picker);
for reason in RescanReason::ALL {
for _ in 0..delta.count(reason) {
println!(
"[{now:>8.2}s] request {reason:<21} files={files} overflow={overflow}"
);
}
let suppressed = delta.count_throttled(reason);
if suppressed > 0 {
println!("[{now:>8.2}s] throttled {reason:<21} x{suppressed}");
}
}
last = stats;
}
if run_for.is_some_and(|limit| watching_since.elapsed() >= limit) {
break;
}
}
let elapsed = watching_since.elapsed();
let stats = picker.rescan_stats();
println!("\n{:.1}s watched", elapsed.as_secs_f64());
println!("{stats}");
if stats.watcher_triggered() > 0 {
println!(
"{:.1} watcher rescan requests/minute",
stats.watcher_triggered() as f64 / elapsed.as_secs_f64().max(1.0) * 60.0
);
} else {
println!("no full rescans: every change was applied incrementally");
}
if stats.throttled > 0 {
println!(
"{} additional request(s) were throttled; {} total requests observed",
stats.throttled,
stats.total + stats.throttled
);
}
if let Ok(mut guard) = picker.write() {
guard.take();
}
Ok(())
}
fn parse_args() -> Result<(String, Option<Duration>), Box<dyn std::error::Error>> {
let mut base_path = None;
let mut run_for = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--seconds" | "-s" => {
let value = args.next().ok_or("--seconds needs a value")?;
run_for = Some(Duration::from_secs(value.parse()?));
}
"--help" | "-h" => {
println!("usage: rescan_probe [path] [--seconds N]");
std::process::exit(0);
}
other => base_path = Some(other.to_string()),
}
}
let base_path = match base_path {
Some(path) => path,
None => std::env::current_dir()?.to_string_lossy().into_owned(),
};
Ok((base_path, run_for))
}
fn live_files(picker: &SharedFilePicker) -> usize {
picker
.read()
.ok()
.and_then(|g| g.as_ref().map(|p| p.live_file_count()))
.unwrap_or(0)
}
fn overflow_files(picker: &SharedFilePicker) -> usize {
picker
.read()
.ok()
.and_then(|g| g.as_ref().map(|p| p.get_overflow_files().len()))
.unwrap_or(0)
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-python"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
[lints]
@@ -17,7 +17,7 @@ ripgrep = ["fff/ripgrep", "fff-query-parser/ripgrep"]
zlob = ["fff/zlob", "fff-query-parser/zlob"]
[dependencies]
fff = { package = "fff-search", path = "../fff-core", version = "0.10.0", default-features = false }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.0", default-features = false }
fff = { package = "fff-search", path = "../fff-core", version = "0.10.5", default-features = false }
fff-query-parser = { path = "../fff-query-parser", version = "0.10.5", default-features = false }
git2 = { workspace = true }
pyo3 = { version = "0.24.0", features = ["extension-module", "abi3-py310"] }
+4 -8
View File
@@ -693,15 +693,11 @@ impl FileFinder {
}
let pattern_refs: Vec<&str> = patterns.iter().map(|s| s.as_str()).collect();
let parsed_constraints = constraints.as_ref().map(|c| {
if picker.mode().is_ai() {
QueryParser::new(fff_query_parser::AiGrepConfig).parse(c)
} else {
fff::grep::parse_grep_query(c)
}
});
let parsed_constraints = constraints
.as_ref()
.map(|c| QueryParser::new(fff_query_parser::AiGrepConfig).parse_constraints(c));
let constraint_refs: &[fff::Constraint<'_>] = match &parsed_constraints {
Some(q) => &q.constraints,
Some(constraints) => constraints,
None => &[],
};
let options = grep_options(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "fff-query-parser"
version = "0.10.0"
version = "0.10.5"
edition = "2024"
description = "Query parser for fff file finder - includes specific syntax for various constraints like globs, extensions, regex etc"
license = "MIT"
+82 -3
View File
@@ -49,6 +49,14 @@ impl<C: ParserConfig> QueryParser<C> {
Self { config }
}
/// Parse a field containing only constraints.
pub fn parse_constraints<'a>(&self, query: &'a str) -> ConstraintVec<'a> {
query
.split_whitespace()
.filter_map(|token| parse_token(token, &self.config))
.collect()
}
pub fn parse<'a>(&self, query: &'a str) -> FFFQuery<'a> {
let raw_query = query;
let config: &C = &self.config;
@@ -364,8 +372,11 @@ fn parse_negation<'a, C: ParserConfig>(token: &'a str, config: &C) -> Option<Con
return Some(Constraint::Not(Box::new(inner_constraint)));
}
// If it's not a special constraint, treat it as negated text
// For backward compatibility with !test syntax
// Negated text (!test) requires ≥3 inner chars with at least one alphanumeric,
// so operators like `!=`, `!==`, `!!` stay literal search text.
if inner_token.len() < 3 || !inner_token.chars().any(|c| c.is_alphanumeric()) {
return None;
}
Some(Constraint::Not(Box::new(Constraint::Text(inner_token))))
}
@@ -495,7 +506,7 @@ fn parse_git_status(value: &str) -> Option<Constraint<'_>> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{FileSearchConfig, GrepConfig};
use crate::{AiGrepConfig, FileSearchConfig, GrepConfig};
/// File-picker-like config with filename-constraint detection enabled,
/// mirroring the Neovim layer's opt-in behavior.
@@ -612,6 +623,38 @@ mod tests {
}
}
#[test]
fn test_negation_operators_stay_literal() {
let parser = QueryParser::new(GrepConfig);
// Operator-like tokens must not become exclusion constraints
for query in [
"Ordering::Acquire) != delivery.epoch",
"a !== b",
"x !! y",
"foo !~ bar",
] {
let result = parser.parse(query);
assert!(
result.constraints.is_empty(),
"{query:?} produced constraints {:?}",
result.constraints
);
assert_eq!(result.grep_text(), query, "grep text must equal raw query");
}
}
#[test]
fn test_negation_short_text_stays_literal() {
let parser = QueryParser::new(FileSearchConfig);
// Inner text < 3 chars is not a Not constraint
let result = parser.parse("!ab foo");
assert!(result.constraints.is_empty());
// Inner text >= 3 chars still is
let result = parser.parse("!abc foo");
assert_eq!(result.constraints.len(), 1);
assert!(matches!(&result.constraints[0], Constraint::Not(_)));
}
#[test]
fn test_negation_extension() {
let parser = QueryParser::new(FileSearchConfig);
@@ -984,6 +1027,42 @@ mod tests {
assert_eq!(result.grep_text(), "pattern");
}
#[test]
fn test_standalone_constraints_preserve_directory() {
let result = QueryParser::new(AiGrepConfig).parse_constraints("scope-a/");
assert_eq!(result.as_slice(), &[Constraint::PathSegment("scope-a")]);
}
#[test]
fn test_plain_constraints_preserve_directory_only() {
let directory = QueryParser::new(GrepConfig).parse_constraints("scope-a/");
assert_eq!(directory.as_slice(), &[Constraint::PathSegment("scope-a")]);
let file = QueryParser::new(GrepConfig).parse_constraints("scope-a/one.txt");
assert!(file.is_empty());
}
#[test]
fn test_standalone_constraints_preserve_file() {
let result = QueryParser::new(AiGrepConfig).parse_constraints("scope-a/one.txt");
assert_eq!(
result.as_slice(),
&[Constraint::FilePath("scope-a/one.txt")]
);
}
#[test]
fn test_standalone_constraints_preserve_file_without_search_text() {
let result = QueryParser::new(AiGrepConfig).parse_constraints("scope-a/ scope-a/one.txt");
assert_eq!(
result.as_slice(),
&[
Constraint::PathSegment("scope-a"),
Constraint::FilePath("scope-a/one.txt")
]
);
}
#[test]
fn test_ai_grep_filename_with_pathsegment_only_promotes_to_text() {
// When the ONLY non-text constraints are path-scoping (PathSegment,
+13 -6
View File
@@ -1,5 +1,5 @@
*fff.nvim.txt*
For Neovim >= 0.10.0 Last change: 2026 July 16
For Neovim >= 0.10.0 Last change: 2026 July 30
==============================================================================
Table of Contents *fff.nvim-table-of-contents*
@@ -24,8 +24,9 @@ INSTALLATION ~
LAZY.NVIM
>lua
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:Lazy clean`
{
'dmtrKovalenko/fff.nvim',
'dmtrKovalenko/fff',
build = function()
-- downloads a prebuilt binary or falls back to cargo build
require("fff.download").download_or_build_binary()
@@ -59,13 +60,14 @@ LAZY.NVIM
VIM.PACK
>lua
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:packdel fff.nvim`
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff' })
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
if name == 'fff' and (kind == 'install' or kind == 'update') then
if not ev.data.active then vim.cmd.packadd('fff') end
require('fff.download').download_or_build_binary()
end
end,
@@ -199,6 +201,11 @@ Defaults are sensible. Override only what you care about.
-- 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'
-- border = {
-- { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' },
-- { ' ', ' ', ' ', ' ', ' ' },
-- },
flex = { size = 130, wrap = 'top' },
min_list_height = 10, -- do not display anything except the list below this threshold
show_scrollbar = true,
@@ -319,7 +326,7 @@ Both find and grep accept these tokens to refine a query:
- `git:modified`. One of `modified`, `staged`, `deleted`, `renamed`, `untracked`, `ignored`.
- `test/`. Any deeply nested children of `test/`.
- `!something`, `!test/`, `!git:modified`. Exclusion.
- `!something`, `!test/`, `!git:modified`. Exclusion. Text exclusions need at least 3 alphanumeric-containing characters, so operators like `!=` or `!==` work.
- `./**/*.{rs,lua}`. Any valid glob, powered by zlob <https://github.com/dmtrKovalenko/zlob>.
Grep-only:
+6 -6
View File
@@ -1,5 +1,5 @@
-- Single file Neovim config for testing fff.nvim locally
-- Usage: nvim -u /Users/neogoose/dev/fff.nvim/init.lua
-- Single file Neovim config for testing fff locally
-- Usage: nvim -u /Users/neogoose/dev/fff/init.lua
-- Set up lazy.nvim plugin manager
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
@@ -17,8 +17,8 @@ vim.opt.rtp:prepend(lazypath)
require('lazy').setup({
{
dir = '~/dev/fff.nvim',
'https://github.com/dmtrKovalenko/fff.nvim',
dir = '~/dev/fff',
'https://github.com/dmtrKovalenko/fff',
build = function()
-- this will download prebuild binary or try to use existing rustup toolchain to build from source
-- (if you are using lazy you can use gb for rebuilding a plugin if needed)
@@ -34,7 +34,7 @@ require('lazy').setup({
},
config = function()
require('fff').setup({
-- Configure fff.nvim here
-- Configure fff here
ui = {
width = 0.8,
height = 0.8,
@@ -59,4 +59,4 @@ vim.keymap.set('n', 'fg', function() require('fff').find_in_git_root() end, { de
vim.keymap.set('n', 'fr', function() require('fff').scan_files() end, { desc = 'Rescan files' })
vim.keymap.set('n', 'fs', function() require('fff').refresh_git_status() end, { desc = 'Refresh git status' })
vim.notify('FFF.nvim local config loaded! Press ff', vim.log.levels.INFO)
vim.notify('FFF local config loaded! Press ff', vim.log.levels.INFO)
Generated
+12 -12
View File
@@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
"lastModified": 1773857772,
"narHash": "sha256-5xsK26KRHf0WytBtsBnQYC/lTWDhQuT57HJ7SzuqZcM=",
"lastModified": 1785782307,
"narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=",
"owner": "ipetkov",
"repo": "crane",
"rev": "b556d7bbae5ff86e378451511873dfd07e4504cd",
"rev": "2c71e194474d13de031d729b729c968ddbe3507f",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1776329215,
"narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=",
"lastModified": 1786593342,
"narHash": "sha256-smTKQXMLLStzc8zJevMCckbk3My7SvbbLmPYZUJJKW4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b86751bc4085f48661017fa226dee99fab6c651b",
"rev": "6b5e5b7a6631f065bf6908986990b37d845f847f",
"type": "github"
},
"original": {
@@ -81,11 +81,11 @@
]
},
"locked": {
"lastModified": 1773803479,
"narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=",
"lastModified": 1786762605,
"narHash": "sha256-iQpYIhInh8gRx+cSnPtX+Yp2Gg9kr2h+MehETCqRgDo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c",
"rev": "ad8ebb59d84bcf3780c46f107e1d99eb4ca2fe7f",
"type": "github"
},
"original": {
@@ -134,11 +134,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1776789209,
"narHash": "sha256-G6B7Q4TXn7MZ1mB+f9rymjsYF5PLWoSvmbxijb/99bw=",
"lastModified": 1786753251,
"narHash": "sha256-7UGNbG2MwgYCdsknKBL6wMXQQeiTmCiz+JS9zvcmqv4=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "14fe971844e841297ddd2ce9783d6892b467af39",
"rev": "a07cc99cee149044f27d95500717161fca2afdf6",
"type": "github"
},
"original": {
+5 -1
View File
@@ -1,5 +1,5 @@
{
description = "fff.nvim";
description = "fff";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
@@ -63,6 +63,10 @@
];
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
# in zlob we compile by default with -target=native unless CI is detected, because
# nix is different we have to make sure that the build target is a baseline cpu
CI = "1";
# Zig 0.16 insists on writing to its global cache even when the
# zlob build.rs passes --global-cache-dir. In the nix sandbox $HOME
# is /homeless-shelter (unwritable), so redirect to $TMPDIR before
+3 -3
View File
@@ -4,7 +4,7 @@
FFF MCP Server installer for Windows.
.DESCRIPTION
Pipe usage:
irm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex
irm https://raw.githubusercontent.com/dmtrKovalenko/fff/main/install-mcp.ps1 | iex
Direct usage (supports params):
iwr https://.../install-mcp.ps1 -OutFile install-mcp.ps1; .\install-mcp.ps1 -Version v0.1.2
Env-var fallbacks (for the piped form):
@@ -29,7 +29,7 @@ $ErrorActionPreference = 'Stop'
# Force TLS 1.2 — PS 5.1 on older Win10 may default to SSL3/TLS1.0 which GitHub rejects.
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$Repo = 'dmtrKovalenko/fff.nvim'
$Repo = 'dmtrKovalenko/fff'
$BinaryName = 'fff-mcp'
if (-not $InstallDir) { $InstallDir = Join-Path $env:LOCALAPPDATA 'fff-mcp\bin' }
@@ -209,7 +209,7 @@ function Show-SetupInstructions {
if (Get-Command codex -ErrorAction SilentlyContinue) {
$foundAny = $true
Write-Success "[Codex] detected"
Write-Host "codex mcp add fff -- fff-mcp"
Write-Host "codex mcp add fff -- `"$BinaryPath`""
Write-Host ""
}
+10 -10
View File
@@ -2,20 +2,20 @@
set -eo pipefail
# FFF MCP Server installer
# Usage: curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.sh | bash
# Usage: curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff/main/install-mcp.sh | bash
REPO="dmtrKovalenko/fff.nvim"
REPO="dmtrKovalenko/fff"
BINARY_NAME="fff-mcp"
INSTALL_DIR="${FFF_MCP_INSTALL_DIR:-$HOME/.local/bin}"
PINNED_RELEASE_TAG="v0.9.6"
PINNED_RELEASE_TAG="v0.10.5"
SHA256_X86_64_UNKNOWN_LINUX_MUSL="102ceaf173ef776becb3322216e9f6b5caef997c400c5d15f112ce4de40a1f5a"
SHA256_AARCH64_UNKNOWN_LINUX_MUSL="a9810c9056afa6d9e8ac1e7a3f1f15f8ecbfdc16a592e9d26e4b2434ef97a675"
SHA256_X86_64_APPLE_DARWIN="58259324c2c13a1b6f24f13138c2cd3eae9ff20e05201a539beb8f2044a651aa"
SHA256_AARCH64_APPLE_DARWIN="29a7fadeafb062f3e5954b1ab8c69e14dca24f5e061cd8d3b1ea1bab385a3754"
SHA256_X86_64_PC_WINDOWS_MSVC="7ff688d034aa42ff779a61ad12689794bdc253c895152796046f374390fb9cad"
SHA256_AARCH64_PC_WINDOWS_MSVC="94ce316a38775d8ed3b32882d8ef45560740319de8d7ac7bb80c31887b3afb6d"
SHA256_X86_64_UNKNOWN_LINUX_MUSL="6ab4f411eeee83e7e3900450b23bb50172e4f93d24dc720265126f0c3a1b1f23"
SHA256_AARCH64_UNKNOWN_LINUX_MUSL="e5e315a57dc9b282e2105172fb299883e1cf9bb63c2a36dd493f23763c8fdd4d"
SHA256_X86_64_APPLE_DARWIN="c81e22a7fb657731a7441be877c622a110eb7e7dbc94e3b57d79c9cea91f38db"
SHA256_AARCH64_APPLE_DARWIN="2f1a75c2401e5df7f5a10827f9ce9fe8401f290ff99c3f3f1423c7d7b156d849"
SHA256_X86_64_PC_WINDOWS_MSVC="3fa325ab186e331d4e0e04c38272931b5877f18016e6eb847cdb80dc9a5465c4"
SHA256_AARCH64_PC_WINDOWS_MSVC="c5415532417aa0594f5e9ebcf80904f9c6503be995f055788aa022b8353a48b1"
expected_sha_for() {
case "$1" in
@@ -262,7 +262,7 @@ print_setup_instructions() {
found_any=true
success "[Codex] detected"
echo ""
echo "codex mcp add fff -- fff-mcp"
echo "codex mcp add fff -- \"${binary_path}\""
echo ""
fi
+1 -1
View File
@@ -9,7 +9,7 @@ local M = {}
--- @field min_list_height number
--- @field show_scrollbar boolean
--- @field path_shorten_strategy string
--- @field border? 'single'|'double'|'rounded'|'solid'|'shadow'|'none' Border preset; falls back to `vim.o.winborder` when nil
--- @field border? 'single'|'double'|'rounded'|'solid'|'shadow'|'none'|table<string[],string[]> Border preset; falls back to `vim.o.winborder` when nil
--- @class FffPreviewConfig
--- @field enabled boolean
+81 -35
View File
@@ -3,6 +3,8 @@ if not fuzzy then error('Failed to load fff.fuzzy module. Ensure the Rust backen
local M = {}
local fs_scanning_refusal
---@class fff.core.State
local state = {
---@type boolean
@@ -111,6 +113,13 @@ M.change_indexing_directory = function(new_path)
local fff_rust = M.ensure_initialized()
local config = require('fff.conf').get()
local refusal = fs_scanning_refusal(vim.tbl_extend('force', config, { base_path = expanded_path }))
if refusal then
vim.notify('FFF: ' .. refusal, vim.log.levels.WARN)
return false
end
local ok, err = pcall(fff_rust.restart_index_in_path, expanded_path, {
follow_symlinks = config.follow_symlinks,
enable_fs_root_scanning = config.enable_fs_root_scanning,
@@ -126,51 +135,88 @@ M.change_indexing_directory = function(new_path)
return true
end
--- Reset the file-picker flag so the next `ensure_initialized` recreates the
--- Rust picker. Call after `cleanup_file_picker` drops it (`FFFClearCache`);
--- otherwise the flag stays set and every later call operates on a dropped
--- picker (see #772).
M.mark_file_picker_uninitialized = function() state.file_picker_initialized = false end
M.ensure_initialized = function()
if state.initialized then return fuzzy end
state.initialized = true
local config = require('fff.conf').get()
if config.logging.enabled then
local log_success, log_error =
pcall(fuzzy.init_tracing, config.logging.log_file, config.logging.log_level, config.logging.retain_runs)
if log_success then
M.log_file_path = log_error
else
vim.notify('Failed to initialize logging: ' .. (tostring(log_error) or 'unknown error'), vim.log.levels.WARN)
end
end
local frecency_db_path = config.frecency.db_path or (vim.fn.stdpath('cache') .. '/fff_frecency')
local history_db_path = config.history.db_path or (vim.fn.stdpath('data') .. '/fff_history')
local ok, result = pcall(fuzzy.init_db, frecency_db_path, history_db_path, true)
if not ok then vim.notify('Failed to databases: ' .. tostring(result), vim.log.levels.WARN) end
ok, result = pcall(fuzzy.init_file_picker, config.base_path, {
follow_symlinks = config.follow_symlinks,
enable_fs_root_scanning = config.enable_fs_root_scanning,
enable_home_dir_scanning = config.enable_home_dir_scanning,
enable_filename_constraint = config.grep and config.grep.enable_filename_constraint,
})
if not ok then
vim.notify('Failed to initialize file picker: ' .. tostring(result), vim.log.levels.ERROR)
-- Refusal gates both one-time setup and (re)creating the picker so we never
-- index fs-root / home, even after a cache clear.
-- Some folks are complaining that neovim instance is closing if ffi returns error on startup (via lazy=false)
-- I can't repro so just precheck on lua side to prevent crashing neovim instance
local refusal = fs_scanning_refusal(config)
if refusal then
state.initialized = true
vim.notify('FFF: ' .. refusal, vim.log.levels.WARN)
return fuzzy
end
state.file_picker_initialized = true
setup_global_autocmds(config)
if not state.initialized then
state.initialized = true
if config.logging.enabled then
local log_success, log_error =
pcall(fuzzy.init_tracing, config.logging.log_file, config.logging.log_level, config.logging.retain_runs)
if log_success then
M.log_file_path = log_error
else
vim.notify('Failed to initialize logging: ' .. (tostring(log_error) or 'unknown error'), vim.log.levels.WARN)
end
end
local highlights = require('fff.highlights')
highlights.setup()
local ok, result = pcall(fuzzy.init_db, config.frecency.db_path, config.history.db_path, true)
if not ok then vim.notify('Failed to databases: ' .. tostring(result), vim.log.levels.WARN) end
vim.api.nvim_create_autocmd('ColorScheme', {
group = vim.api.nvim_create_augroup('fff_highlights', { clear = true }),
callback = function() highlights.setup() end,
desc = 'Re-apply FFF highlights on colorscheme change',
})
setup_global_autocmds(config)
local highlights = require('fff.highlights')
highlights.setup()
vim.api.nvim_create_autocmd('ColorScheme', {
group = vim.api.nvim_create_augroup('fff_highlights', { clear = true }),
callback = function() highlights.setup() end,
desc = 'Re-apply FFF highlights on colorscheme change',
})
end
-- Recreated whenever the picker was torn down (e.g. `FFFClearCache files`).
-- Guarded separately from one-time setup so a cache clear rebuilds the
-- picker instead of leaving a dropped one behind (#772).
if not state.file_picker_initialized then
local ok, result = pcall(fuzzy.init_file_picker, config.base_path, {
follow_symlinks = config.follow_symlinks,
enable_fs_root_scanning = config.enable_fs_root_scanning,
enable_home_dir_scanning = config.enable_home_dir_scanning,
enable_filename_constraint = config.grep and config.grep.enable_filename_constraint,
})
if not ok then
vim.notify('Failed to initialize file picker: ' .. tostring(result), vim.log.levels.ERROR)
return fuzzy
end
state.file_picker_initialized = true
end
return fuzzy
end
function fs_scanning_refusal(config)
local path = vim.fn.fnamemodify(vim.fn.expand(config.base_path), ':p'):gsub('/+$', '')
if not config.enable_fs_root_scanning and (path == '' or path:match('^%a:$')) then
return 'Refusing to index filesystem root. Set enable_fs_root_scanning = true to override.'
end
if not config.enable_home_dir_scanning then
local home = (vim.fn.expand('$HOME') or ''):gsub('/+$', '')
if home ~= '' and path == home then
return 'Refusing to index home directory. Set enable_home_dir_scanning = true to override.'
end
end
return nil
end
return M
+1 -1
View File
@@ -3,7 +3,7 @@ local system = require('fff.utils.system')
local fs_utils = require('fff.utils.fs')
local fff_version = require('fff.utils.version')
local GITHUB_REPO = 'dmtrKovalenko/fff.nvim'
local GITHUB_REPO = 'dmtrKovalenko/fff'
local function get_binary_dir(plugin_dir) return plugin_dir .. '/../target/release' end
+3
View File
@@ -29,6 +29,9 @@ local function get_border_chars(config)
if border == nil or border == '' then border = vim.o.winborder end
if border == nil or border == '' then border = 'single' end
if type(border) == 'table' then
if #border == 2 and type(border[1]) == 'table' and type(border[2]) == 'table' then return border[1], border[2] end
end
if BORDER_PRESETS[border] then return BORDER_PRESETS[border], T_JUNCTION_PRESETS[border] end
return BORDER_PRESETS.single, T_JUNCTION_PRESETS.single
end
+7 -1
View File
@@ -101,7 +101,13 @@ function M.clear_cache(scope)
if scope == 'all' or scope == 'files' then
local ok, err = pcall(fuzzy.cleanup_file_picker)
if not ok then table.insert(errors, 'cleanup file picker: ' .. tostring(err)) end
if not ok then
table.insert(errors, 'cleanup file picker: ' .. tostring(err))
else
-- Rust picker is gone; clear the core flag so the next ensure_initialized
-- rebuilds it instead of operating on a dropped picker (#772).
require('fff.core').mark_file_picker_uninitialized()
end
end
if scope == 'all' or scope == 'frecency' then
-2
View File
@@ -9,8 +9,6 @@ local fuzzy = require('fff.fuzzy')
local file_renderer = require('fff.picker_ui.file_renderer')
local tresitter_highlight = require('fff.treesitter_hl')
-- ===== Search Bridge =====
---@class fff.grep.SearchResult
---@field items table[] Array of grep match items
---@field total_matched number Total matches found in this call
-4241
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,17 +1,6 @@
{
"private": true,
"workspaces": ["packages/fff-bun", "packages/fff-node", "packages/pi-fff"],
"pi": {
"extensions": ["./packages/pi-fff/src/index.ts"]
},
"scripts": {
"format": "biome format --write packages",
"format:check": "biome format packages",
"lint": "biome lint packages",
"check": "biome check --write packages",
"check:ci": "biome check packages"
},
"devDependencies": {
"@biomejs/biome": "^2.4.4"
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"printWidth": 90,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"arrowParens": "always",
"bracketSpacing": true,
"ignorePatterns": [
"*/dist",
"fff-bin-*",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.json",
"**/*.md",
"**/*.toml"
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc"],
"categories": {
"correctness": "error"
},
"env": {
"builtin": true
},
"ignorePatterns": ["*/dist", "fff-bin-*", "**/*.mjs", "**/*.cjs", "**/*.js"],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/no-non-null-assertion": "off",
"unicorn/no-array-for-each": "off"
}
}
+635
View File
@@ -0,0 +1,635 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"devDependencies": {
"oxfmt": "0.63.0",
"oxlint": "1.78.0",
},
},
"fff-bun": {
"name": "@ff-labs/fff-bun",
"version": "0.1.37",
"devDependencies": {
"@types/bun": "^1.3.8",
"typescript": "^5.0.0",
},
},
"fff-node": {
"name": "@ff-labs/fff-node",
"version": "0.1.37",
"dependencies": {
"ffi-rs": "^1.0.0",
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.5.0",
"typescript": "^5.0.0",
},
},
"pi-fff": {
"name": "@ff-labs/pi-fff",
"version": "0.6.0",
"dependencies": {
"@ff-labs/fff-bun": "*",
"@ff-labs/fff-node": "*",
},
"devDependencies": {
"@types/bun": "^1.3.8",
"@types/node": "^22.0.0",
"typescript": "^5.0.0",
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*",
"@sinclair/typebox": "*",
},
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="],
"@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-login": "^3.972.76", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.13", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/token-providers": "3.1111.0", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.75", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw=="],
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.33", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-1Dd5WyEE2Kb3HvY44u7Ob16ST2W6iutOqsQ8Y2hUmsL2mAH/STlGS1dS9h3IOE6L7Ld3AR2HzKJ6XeCMOw8Peg=="],
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.28", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Z1EDXnS01P7H5jVrUx+/dBqV0m7dta7bSxLclkOuDuS93pNNQm0IcT4YLUbuvWKPYNxbI8aTG0p5Br30GSKDgA=="],
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-jdgP3jR5Q96j1jjZ98GGwpGg1CBNFIO2YE+vXg8cg8PvNY4NvgQNYJsqDaRX2PYv5gSUX/+C0D58Fhspj9ELMQ=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.43", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.45", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="],
"@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.10", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.84.2", "", { "dependencies": { "@earendil-works/pi-ai": "^0.84.2", "@earendil-works/pi-telemetry": "^0.84.2", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", "yaml": "2.9.0" } }, "sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA=="],
"@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.84.2", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.40.0", "partial-json": "0.1.7", "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig=="],
"@earendil-works/pi-client": ["@earendil-works/pi-client@0.84.2", "", { "dependencies": { "@earendil-works/pi-protocol": "^0.84.2" } }, "sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg=="],
"@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.84.2", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.84.2", "@earendil-works/pi-ai": "^0.84.2", "@earendil-works/pi-client": "^0.84.2", "@earendil-works/pi-protocol": "^0.84.2", "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "grok-mermaid": "0.2.2", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", "undici": "8.9.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA=="],
"@earendil-works/pi-protocol": ["@earendil-works/pi-protocol@0.84.2", "", { "dependencies": { "typebox": "1.3.7" } }, "sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ=="],
"@earendil-works/pi-telemetry": ["@earendil-works/pi-telemetry@0.84.2", "", {}, "sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g=="],
"@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.84.2", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@workspace:fff-bun"],
"@ff-labs/fff-node": ["@ff-labs/fff-node@workspace:fff-node"],
"@ff-labs/pi-fff": ["@ff-labs/pi-fff@workspace:pi-fff"],
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="],
"@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="],
"@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="],
"@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="],
"@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="],
"@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="],
"@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="],
"@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="],
"@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="],
"@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="],
"@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="],
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ=="],
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A=="],
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA=="],
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg=="],
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg=="],
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw=="],
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw=="],
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg=="],
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg=="],
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw=="],
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg=="],
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA=="],
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA=="],
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ=="],
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw=="],
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g=="],
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g=="],
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.78.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.78.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
"@smithy/core": ["@smithy/core@3.33.2", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw=="],
"@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
"@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OueBlUFBT9IwD9pQnoYs0UszRBEySskfrEPXXfvKfGjL/DXnfn6kUheQ3oIP6sSmshVGNQUwrTCPo6feAa4QjA=="],
"@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-x4mxXOKwSgwYx6OBAKrW+Ocp8um2KoPpUwaD0Z2tmR6EekoRvFGN7s/eEK1avqCxcsLN7/MzGOnCCFyGpOVfPA=="],
"@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.7", "", { "os": "linux", "cpu": "arm" }, "sha512-9r//Z022QYVoIAY458Dpk8HWkisk/y4NJC3d52RlkdsLD+9p0zn0cKKSoMF/xUsOWDFSmOf+cwU/Xadi/wxFMQ=="],
"@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nrW4MlFlyQInfxTsI5wtcIpbV4KlHLWo2BCk4CHIETlUT+PUspNFqDg+Byy7My3hpCZlXp0WsMVP/3N0LlybNw=="],
"@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-eLLx4P8DzNnuPif19nAvKPaf88lSf6KFbDetH3nAIRZvI495h0jigN835Ayr7uAADssohGfO9MPMH33DXmFErA=="],
"@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-upEz1Q98T51x2In872fsjgHCJbE3e8r9JxhJD+5NvSMk6BpieYWRodTbsMxV6oKWmfl12loKwF2j4OBSWKl0lA=="],
"@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-YOcOkIwVvIhpwuGJ428h15vpE0KnT3teFG0g2J31FrBxrVh9IH4HeTGXjelI3l9SI5gyiZDnD4PQnQ1U6oqTcA=="],
"@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZfYzLV1w7Mhzh9XRScEMpw9oazIEiwO+0+TYHHRL882d5oD0Q99YikmRhdifsoHCCQefKmWF/4BXCeFkzg8NFA=="],
"@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.7", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-ifYz+f+giKpT39lKWDru+om6QnA9J7J953ns60xHgB46ESZvPd/Y2W5+PesRNC9U+vnFEiLZ9CNC+R3fBWC1pg=="],
"@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-H3s4wOLtZtKtmuPjUNE+bhnSKxxpXcuxG70jRC8dUMHqIm5iCvdoZ4XYDeq7/uUYlySerXFmRL4dAVCX/TUNTg=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"ffi-rs": ["ffi-rs@1.3.7", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-x64": "1.3.7", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.7", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.7", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-x64-musl": "1.3.7", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.7", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.7", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.7" } }, "sha512-5MMZQS2t/6f/ec3sx0f51gBcKGZPGfolPAW04lvLOSYnr7gmOKs8YzH1Rh+34WGO+VtcgcTuxaUUoJLcR4Tirg=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"grok-mermaid": ["grok-mermaid@0.2.2", "", {}, "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA=="],
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"openai": ["openai@6.40.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"] }, "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA=="],
"oxfmt": ["oxfmt@0.63.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.63.0", "@oxfmt/binding-android-arm64": "0.63.0", "@oxfmt/binding-darwin-arm64": "0.63.0", "@oxfmt/binding-darwin-x64": "0.63.0", "@oxfmt/binding-freebsd-x64": "0.63.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", "@oxfmt/binding-linux-arm64-gnu": "0.63.0", "@oxfmt/binding-linux-arm64-musl": "0.63.0", "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", "@oxfmt/binding-linux-riscv64-musl": "0.63.0", "@oxfmt/binding-linux-s390x-gnu": "0.63.0", "@oxfmt/binding-linux-x64-gnu": "0.63.0", "@oxfmt/binding-linux-x64-musl": "0.63.0", "@oxfmt/binding-openharmony-arm64": "0.63.0", "@oxfmt/binding-win32-arm64-msvc": "0.63.0", "@oxfmt/binding-win32-ia32-msvc": "0.63.0", "@oxfmt/binding-win32-x64-msvc": "0.63.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw=="],
"oxlint": ["oxlint@1.78.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.78.0", "@oxlint/binding-android-arm64": "1.78.0", "@oxlint/binding-darwin-arm64": "1.78.0", "@oxlint/binding-darwin-x64": "1.78.0", "@oxlint/binding-freebsd-x64": "1.78.0", "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", "@oxlint/binding-linux-arm-musleabihf": "1.78.0", "@oxlint/binding-linux-arm64-gnu": "1.78.0", "@oxlint/binding-linux-arm64-musl": "1.78.0", "@oxlint/binding-linux-ppc64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-musl": "1.78.0", "@oxlint/binding-linux-s390x-gnu": "1.78.0", "@oxlint/binding-linux-x64-gnu": "1.78.0", "@oxlint/binding-linux-x64-musl": "1.78.0", "@oxlint/binding-openharmony-arm64": "1.78.0", "@oxlint/binding-win32-arm64-msvc": "1.78.0", "@oxlint/binding-win32-ia32-msvc": "1.78.0", "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A=="],
"@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g=="],
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
}
}
@@ -0,0 +1,18 @@
{
"name": "@ff-labs/fff-bin-android-arm64",
"version": "0.0.0",
"description": "fff native binary for Android ARM64 (Termux)",
"os": ["android"],
"cpu": ["arm64"],
"main": "libfff_c.so",
"files": ["libfff_c.so"],
"publishConfig": {
"access": "public"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/dmtrKovalenko/fff.git",
"directory": "packages/fff-bin-android-arm64"
}
}
+2 -2
View File
@@ -174,8 +174,8 @@ If prebuilt binaries aren't available for your platform:
```bash
# Clone the repository
git clone https://github.com/dmtrKovalenko/fff.nvim
cd fff.nvim
git clone https://github.com/dmtrKovalenko/fff
cd fff
# Build the C library
cargo build --release -p fff-c
-61
View File
@@ -1,61 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "fff",
"devDependencies": {
"@types/bun": "^1.3.8",
"typescript": "^5.0.0",
},
"optionalDependencies": {
"@ff-labs/fff-bun-darwin-arm64": "0.0.0",
"@ff-labs/fff-bun-darwin-x64": "0.0.0",
"@ff-labs/fff-bun-linux-arm64-gnu": "0.0.0",
"@ff-labs/fff-bun-linux-arm64-musl": "0.0.0",
"@ff-labs/fff-bun-linux-x64-gnu": "0.0.0",
"@ff-labs/fff-bun-linux-x64-musl": "0.0.0",
"@ff-labs/fff-bun-win32-arm64": "0.0.0",
"@ff-labs/fff-bun-win32-x64": "0.0.0",
},
"peerDependencies": {
"bun": ">=1.0.0",
},
},
},
"packages": {
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw=="],
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA=="],
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg=="],
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g=="],
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ=="],
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg=="],
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw=="],
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w=="],
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w=="],
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q=="],
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="],
"bun": ["bun@1.3.9", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.9", "@oven/bun-darwin-x64": "1.3.9", "@oven/bun-darwin-x64-baseline": "1.3.9", "@oven/bun-linux-aarch64": "1.3.9", "@oven/bun-linux-aarch64-musl": "1.3.9", "@oven/bun-linux-x64": "1.3.9", "@oven/bun-linux-x64-baseline": "1.3.9", "@oven/bun-linux-x64-musl": "1.3.9", "@oven/bun-linux-x64-musl-baseline": "1.3.9", "@oven/bun-windows-x64": "1.3.9", "@oven/bun-windows-x64-baseline": "1.3.9" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-v5hkh1us7sMNjfimWE70flYbD5I1/qWQaqmJ45q2qk5H/7muQVa478LSVRSFyGTBUBog2LsPQnfIRdjyWJRY+A=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}
+12 -16
View File
@@ -4,19 +4,24 @@
"private": false,
"description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
"types": "./dist/index.d.ts",
"bun": "./src/index.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"src",
"examples"
],
"scripts": {
"build": "bun build ./src/index.ts --format=esm --target=bun --sourcemap=external --outdir dist && tsc -p tsconfig.build.json",
"test": "bun test test/",
"typecheck": "tsc --noEmit",
"demo": "bun ./examples/search.ts"
@@ -27,7 +32,8 @@
"os": [
"darwin",
"linux",
"win32"
"win32",
"android"
],
"cpu": [
"x64",
@@ -57,16 +63,6 @@
"url": "https://github.com/dmtrKovalenko/fff/issues"
},
"homepage": "https://github.com/dmtrKovalenko/fff#readme",
"optionalDependencies": {
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
"@ff-labs/fff-bin-win32-x64": "0.0.0",
"@ff-labs/fff-bin-win32-arm64": "0.0.0"
},
"devDependencies": {
"@types/bun": "^1.3.8",
"typescript": "^5.0.0"
+3 -12
View File
@@ -573,16 +573,10 @@ export interface FileFinderApi {
glob(pattern: string, options?: GlobOptions): Result<SearchResult>;
/** Fuzzy directory search. */
directorySearch(
query: string,
options?: DirSearchOptions,
): Result<DirSearchResult>;
directorySearch(query: string, options?: DirSearchOptions): Result<DirSearchResult>;
/** Fuzzy search over files and directories interleaved by score. */
mixedSearch(
query: string,
options?: SearchOptions,
): Result<MixedSearchResult>;
mixedSearch(query: string, options?: SearchOptions): Result<MixedSearchResult>;
/** Content search (live grep). */
grep(query: string, options?: GrepOptions): Result<GrepResult>;
@@ -647,10 +641,7 @@ export interface FileFinderApi {
* Events are debounced and submitted in batches per 100-ms window at most 128 events.
* Gitignored and other ignored files are never triggering watcher.
*/
watch(
callback: WatchBatchCallback,
options?: WatchOptions,
): Result<WatchUnsubscribe>;
watch(callback: WatchBatchCallback, options?: WatchOptions): Result<WatchUnsubscribe>;
watch(
pattern: string,
callback: WatchBatchCallback,
+3
View File
@@ -14,6 +14,8 @@ export function getTriple(): string {
let osName: string;
if (platform === "darwin") {
osName = "apple-darwin";
} else if (platform === "android") {
osName = "linux-android";
} else if (platform === "linux") {
osName = detectLinuxLibc();
} else if (platform === "win32") {
@@ -107,6 +109,7 @@ const TRIPLE_TO_NPM_PACKAGE: Record<string, string> = {
"aarch64-unknown-linux-musl": "@ff-labs/fff-bin-linux-arm64-musl",
"x86_64-pc-windows-msvc": "@ff-labs/fff-bin-win32-x64",
"aarch64-pc-windows-msvc": "@ff-labs/fff-bin-win32-arm64",
"aarch64-linux-android": "@ff-labs/fff-bin-android-arm64",
};
/**
+235
View File
@@ -0,0 +1,235 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { FileFinder } from "../src/index";
// Real-native tests for #700/#760. Lives here, not in pi-fff/test: that suite
// mocks @ff-labs/fff-bun process-globally and bun module mocks can't be undone.
mock.module("@earendil-works/pi-tui", () => ({
Text: class Text {
text: string;
constructor(text: string) {
this.text = text;
}
setText(text: string) {
this.text = text;
}
},
}));
const schema = (type: string) => (options?: unknown) => ({ type, options });
mock.module("@sinclair/typebox", () => ({
Type: {
Array: (items: unknown, options?: unknown) => ({ type: "array", items, options }),
Boolean: schema("boolean"),
Number: schema("number"),
Object: (properties: unknown, options?: unknown) => ({
type: "object",
properties,
options,
}),
Optional: (value: unknown) => ({ ...(value as object), optional: true }),
String: schema("string"),
Union: (items: unknown[], options?: unknown) => ({ type: "union", items, options }),
},
}));
const { default: fffExtension } = await import("../../pi-fff/src/index");
// Inject this package as the extension's SDK through the cache hook sdk.ts
// already uses for reloads: CI has no node_modules to resolve "@ff-labs/fff-bun"
// from pi-fff, and the finder stays the real native one either way.
(globalThis as Record<string, unknown>).__fffSdkPromiseGlobal = Promise.resolve({
FileFinder,
});
const cleanups: Array<() => void> = [];
afterEach(() => {
while (cleanups.length) cleanups.pop()?.();
});
function makeWorkspace(name: string): string {
const dir = mkdtempSync(join(tmpdir(), `pi-fff-native-${name}-`));
cleanups.push(() => rmSync(dir, { recursive: true, force: true }));
writeFileSync(join(dir, "alpha.ts"), "export const alpha = 1;\n");
writeFileSync(join(dir, "beta.ts"), "export const beta = 2;\n");
mkdirSync(join(dir, "src"));
writeFileSync(join(dir, "src", "gamma.ts"), "export const gamma = 3;\n");
return dir;
}
function makeDbPaths() {
const root = mkdtempSync(join(tmpdir(), "pi-fff-native-dbs-"));
cleanups.push(() => rmSync(root, { recursive: true, force: true }));
return { frecencyDbPath: join(root, "frecency"), historyDbPath: join(root, "history") };
}
function createFinder(options: Parameters<typeof FileFinder.create>[0]) {
const result = FileFinder.create(options);
if (result.ok) {
const finder = result.value;
cleanups.push(() => {
if (!finder.isDestroyed) finder.destroy();
});
}
return result;
}
type SearchOk = { ok: true; value: { items: Array<{ fileName: string }> } };
type SearchResult = SearchOk | { ok: false; error: string };
function fileNames(
finder: { fileSearch: (q: string, o: { pageSize: number }) => SearchResult },
query: string,
): string[] {
const search = finder.fileSearch(query, { pageSize: 10 });
expect(search.ok).toBe(true);
return search.ok ? search.value.items.map((i) => i.fileName) : [];
}
describe("fff-bun: many finders share one LMDB env per path (#700/#760)", () => {
test("a second finder on the same db paths works and searches", async () => {
const dbs = makeDbPaths();
const main = createFinder({ basePath: makeWorkspace("main"), ...dbs });
expect(main.ok).toBe(true);
if (!main.ok) return;
await main.value.waitForScan(15_000);
expect(fileNames(main.value, "alpha")).toContain("alpha.ts");
// The createAgentSession scenario: same process, same db paths.
const sub = createFinder({ basePath: makeWorkspace("subagent"), ...dbs });
expect(sub.ok).toBe(true);
if (!sub.ok) return;
await sub.value.waitForScan(15_000);
expect(fileNames(sub.value, "gamma")).toContain("gamma.ts");
}, 30_000);
test("destroying one finder keeps the shared env alive for the other", async () => {
const dbs = makeDbPaths();
const first = createFinder({ basePath: makeWorkspace("first"), ...dbs });
const second = createFinder({ basePath: makeWorkspace("second"), ...dbs });
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
if (!first.ok || !second.ok) return;
first.value.destroy();
await second.value.waitForScan(15_000);
expect(fileNames(second.value, "alpha")).toContain("alpha.ts");
// And once the survivor is gone too, the paths are reusable.
second.value.destroy();
const third = createFinder({ basePath: makeWorkspace("third"), ...dbs });
expect(third.ok).toBe(true);
}, 30_000);
test("a db-less aux finder coexists with the main finder (#700)", async () => {
const main = createFinder({ basePath: makeWorkspace("main"), ...makeDbPaths() });
expect(main.ok).toBe(true);
const aux = createFinder({ basePath: makeWorkspace("aux") });
expect(aux.ok).toBe(true);
if (!aux.ok) return;
await aux.value.waitForScan(15_000);
expect(fileNames(aux.value, "gamma")).toContain("gamma.ts");
}, 30_000);
});
type EventHandler = (...args: unknown[]) => unknown;
type RegisteredTool = {
name: string;
execute: (
toolCallId: string,
params: unknown,
signal?: AbortSignal,
) => Promise<unknown>;
};
function startSession(
cwd: string,
dbs: { frecencyDbPath: string; historyDbPath: string },
) {
const events = new Map<string, EventHandler>();
const tools = new Map<string, RegisteredTool>();
const notifications: Array<{ message: string; level?: string }> = [];
let activeTools: string[] = [];
const flags: Record<string, unknown> = {
"fff-frecency-db": dbs.frecencyDbPath,
"fff-history-db": dbs.historyDbPath,
};
const pi = {
getFlag: (name: string) => flags[name],
on: (event: string, handler: EventHandler) => events.set(event, handler),
registerCommand: () => undefined,
registerFlag: () => undefined,
registerTool: (tool: RegisteredTool) => tools.set(tool.name, tool),
getActiveTools: () => activeTools,
setActiveTools: (names: string[]) => {
activeTools = names;
},
appendEntry: () => undefined,
};
const ctx = {
cwd,
ui: {
notify: (message: string, level?: string) => notifications.push({ message, level }),
setStatus: () => undefined,
},
};
fffExtension(pi as never);
cleanups.push(() => {
void events.get("session_shutdown")?.({}, undefined);
});
return {
start: async () => events.get("session_start")?.({ reason: "startup" }, ctx),
shutdown: async () => events.get("session_shutdown")?.({}, undefined),
find: async (pattern: string, params?: Record<string, unknown>) =>
JSON.stringify(
await tools.get("fffind")?.execute("test-call", { pattern, ...params }),
),
errors: () => notifications.filter((n) => n.level === "error").map((n) => n.message),
};
}
describe("pi-fff: in-process double activation works (#760)", () => {
test("two sessions in one process both search against the same dbs", async () => {
const dbs = makeDbPaths();
const first = startSession(makeWorkspace("session1"), dbs);
await first.start();
expect(first.errors()).toEqual([]);
expect(await first.find("alpha")).toContain("alpha.ts");
// What createAgentSession does: activate the extension again in-process.
const second = startSession(makeWorkspace("session2"), dbs);
await second.start();
expect(second.errors()).toEqual([]);
expect(await second.find("gamma")).toContain("gamma.ts");
// And the first session keeps working alongside it.
expect(await first.find("beta")).toContain("beta.ts");
await second.shutdown();
await first.shutdown();
}, 40_000);
test("aux finder over an external root shares the session dbs (#700)", async () => {
const dbs = makeDbPaths();
const session = startSession(makeWorkspace("aux-session"), dbs);
await session.start();
expect(session.errors()).toEqual([]);
// An absolute out-of-workspace path constraint routes to an aux finder,
// which now opens the same frecency/history LMDB paths as the main finder.
const external = makeWorkspace("aux-external");
expect(await session.find("gamma", { path: external })).toContain("gamma.ts");
expect(session.errors()).toEqual([]);
await session.shutdown();
}, 40_000);
});
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"emitDeclarationOnly": true,
"noEmit": false
},
"include": ["src/**/*"]
}
+2 -2
View File
@@ -152,8 +152,8 @@ If prebuilt binaries aren't available for your platform:
```bash
# Clone the repository
git clone https://github.com/dmtrKovalenko/fff.nvim
cd fff.nvim
git clone https://github.com/dmtrKovalenko/fff
cd fff
# Build the C library
cargo build --release -p fff-c
+16 -18
View File
@@ -4,19 +4,25 @@
"private": false,
"description": "High-performance fuzzy file finder for Node.js - perfect for LLM agent tools",
"type": "module",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/src/index.js",
"types": "./dist/src/index.d.ts"
}
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"build": "npm run build:bundle && npm run build:types",
"build:bundle": "tsup src/index.ts --format esm,cjs --platform node --target node18 --external ffi-rs --sourcemap",
"build:types": "tsc -p tsconfig.build.json",
"test": "node test/e2e.mjs && node test/watch.mjs",
"typecheck": "tsc --noEmit"
},
@@ -26,7 +32,8 @@
"os": [
"darwin",
"linux",
"win32"
"win32",
"android"
],
"cpu": [
"x64",
@@ -59,18 +66,9 @@
"dependencies": {
"ffi-rs": "^1.0.0"
},
"optionalDependencies": {
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
"@ff-labs/fff-bin-win32-x64": "0.0.0",
"@ff-labs/fff-bin-win32-arm64": "0.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^22.0.0"
"@types/node": "^22.0.0",
"tsup": "^8.5.0"
}
}
+4 -1
View File
@@ -16,6 +16,9 @@ import { getLibFilename, getNpmPackageName } from "./platform.js";
* Get the current file's directory
*/
function getCurrentDir(): string {
// CJS build: import.meta.url is inlined at bundle time, __dirname is the truth
if (typeof __dirname !== "undefined") return __dirname;
const url = import.meta.url;
if (url.startsWith("file://")) {
@@ -30,7 +33,7 @@ function getCurrentDir(): string {
function getPackageDir(): string {
const currentDir = getCurrentDir();
// In dev: src/ -> package root
// In dist: dist/src/ -> package root
// In dist: dist/ -> package root
// We look for package.json to find the actual root
let dir = currentDir;
for (let i = 0; i < 5; i++) {

Some files were not shown because too many files have changed in this diff Show More