Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28fce4145a | |||
| d5b4abd2ac | |||
| e6df25388a | |||
| 611dd873a3 | |||
| d7c0ddc374 | |||
| be2dd8d568 | |||
| f4def12a1c | |||
| 459ebcdbdb | |||
| f565d37175 | |||
| 0d8c257a5d | |||
| d01cc483ca | |||
| cf2130090e | |||
| 232288c60b | |||
| 2c9cae2e0f | |||
| a166255d8f | |||
| ffd8eb9c08 | |||
| ed6b40a591 | |||
| c4f6c600d6 | |||
| 787a9b1cb7 | |||
| 6398d32c0c | |||
| c6194b848d | |||
| b71b7cf4ac | |||
| cc289f0f93 | |||
| 2107f038dc | |||
| 9441cbc5ae | |||
| dd87489d6b | |||
| d4c416c2fc | |||
| b6f351d729 |
@@ -23,12 +23,23 @@ env:
|
||||
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:
|
||||
name: e2e (${{ matrix.os }})
|
||||
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:
|
||||
@@ -53,9 +64,19 @@ jobs:
|
||||
cache-key: "v2-lua-e2e"
|
||||
rustflags: ""
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.11
|
||||
with:
|
||||
version: v0.17.0
|
||||
|
||||
- name: Build Rust binary
|
||||
shell: bash
|
||||
run: make build
|
||||
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.os == 'windows-latest'
|
||||
@@ -127,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
|
||||
@@ -177,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 \
|
||||
@@ -193,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -228,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:
|
||||
@@ -294,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:
|
||||
@@ -344,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:
|
||||
@@ -478,7 +509,7 @@ 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:
|
||||
@@ -612,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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
-- luacheck configuration for fff.nvim
|
||||
-- luacheck configuration for fff
|
||||
-- https://luacheck.readthedocs.io/en/stable/config.html
|
||||
|
||||
-- Neovim globals
|
||||
|
||||
Generated
+11
-9
@@ -633,7 +633,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fff-c"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"fff-query-parser",
|
||||
"fff-search",
|
||||
@@ -643,7 +643,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-grep"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"memchr",
|
||||
@@ -651,7 +651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-mcp"
|
||||
version = "0.10.3"
|
||||
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.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"chrono",
|
||||
@@ -703,7 +705,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-python"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"fff-query-parser",
|
||||
"fff-search",
|
||||
@@ -713,7 +715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-query-parser"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"zlob",
|
||||
@@ -721,7 +723,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-search"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"aho-corasick",
|
||||
@@ -3235,9 +3237,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlob"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e7ca1588981ea66f5ac470915c4cb44a47ecfb5797576f65fca1bafacedf4e"
|
||||
checksum = "f0f010d9b10495cc0037cb47cedc143c39c7fa2d59f752dca19036fd0234a210"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bitflags 2.11.0",
|
||||
|
||||
+3
-3
@@ -15,8 +15,8 @@ resolver = "2"
|
||||
module_inception = "allow"
|
||||
|
||||
[workspace.dependencies]
|
||||
fff-grep = { version = "0.10.3", path = "crates/fff-grep" }
|
||||
fff-query-parser = { version = "0.10.3", 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"
|
||||
@@ -36,7 +36,7 @@ ignore = "0.4.22"
|
||||
memmap2 = "0.9"
|
||||
mimalloc = "0.1.47"
|
||||
signal-hook-registry = "1.4"
|
||||
zlob = { version = "=1.6.2" }
|
||||
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
@@ -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.10.1"
|
||||
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 "eff6660a9c48e3e1972d557c1003e057e5ff99d6039f5f810671f2123093fdfc"
|
||||
sha256 "2f1a75c2401e5df7f5a10827f9ce9fe8401f290ff99c3f3f1423c7d7b156d849"
|
||||
end
|
||||
|
||||
on_intel do
|
||||
url "#{RELEASE_BASE}/v#{version}/fff-mcp-x86_64-apple-darwin"
|
||||
sha256 "d3c8d70d47ab80afa2287e1b4546b23f494bb4604b80e5a706a0af714ee25674"
|
||||
sha256 "c81e22a7fb657731a7441be877c622a110eb7e7dbc94e3b57d79c9cea91f38db"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_arm do
|
||||
url "#{RELEASE_BASE}/v#{version}/fff-mcp-aarch64-unknown-linux-gnu"
|
||||
sha256 "ed628d57200e48298fcbeffc0f6d85fb572de02bb900bcc6fc7cc05e33ba2777"
|
||||
sha256 "48f5af0d9e6ef4506c5cccd3f4571da4f0acc9b9c0967832167e49fe651f8ee5"
|
||||
end
|
||||
|
||||
on_intel do
|
||||
url "#{RELEASE_BASE}/v#{version}/fff-mcp-x86_64-unknown-linux-gnu"
|
||||
sha256 "3541dad4663fc9e5b8b97ce8bd20084ddae08a3c6cb828fcfbcbb6ca8c411513"
|
||||
sha256 "0408cbac06303b3bf2041181089d8b4974b527883242bdcf8ba599b41c003973"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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,7 @@ 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.
|
||||
@@ -118,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) \
|
||||
@@ -131,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 \
|
||||
@@ -143,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 \
|
||||
@@ -159,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; \
|
||||
@@ -269,20 +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)'; \
|
||||
} \
|
||||
} \
|
||||
for (const dep of ['@ff-labs/fff-bun', '@ff-labs/fff-node']) { \
|
||||
if (pkg.dependencies?.[dep]) pkg.dependencies[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:
|
||||
@@ -290,7 +282,7 @@ format-rust:
|
||||
format-lua:
|
||||
stylua .
|
||||
format-ts:
|
||||
bun format
|
||||
cd packages && bun format
|
||||
|
||||
format: format-rust format-lua format-ts
|
||||
|
||||
@@ -299,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
|
||||
|
||||
|
||||
@@ -37,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.
|
||||
@@ -49,7 +49,7 @@ 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
|
||||
|
||||
@@ -120,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
|
||||
|
||||
@@ -153,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()
|
||||
@@ -187,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,
|
||||
@@ -614,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
|
||||
|
||||
|
||||
-35
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,414 +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",
|
||||
"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-bun": "*",
|
||||
"@ff-labs/fff-node": "*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/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=="],
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-c"
|
||||
version = "0.10.3"
|
||||
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.3", default-features = false }
|
||||
fff-query-parser = { path = "../fff-query-parser", version = "0.10.3", 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
@@ -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 => &[],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-search"
|
||||
version = "0.10.3"
|
||||
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"
|
||||
|
||||
@@ -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)>>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
pub(crate) mod env_pool;
|
||||
pub(crate) mod lmdb;
|
||||
|
||||
pub mod db_healthcheck;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -229,9 +229,19 @@ pub(crate) fn grep_search<'a>(
|
||||
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: Vec::new(),
|
||||
constraints: scoped_constraints,
|
||||
fuzzy_query: fff_query_parser::FuzzyQuery::Text(raw),
|
||||
location: None,
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]>,
|
||||
|
||||
@@ -123,7 +123,7 @@ mod tests {
|
||||
// Source trees under $HOME stay searchable.
|
||||
for rel in [
|
||||
"dev/chromium/third_party/blink/renderer/core/dom/node.cc",
|
||||
"dev/fff.nvim/crates/fff-core/src/lib.rs",
|
||||
"dev/fff/crates/fff-core/src/lib.rs",
|
||||
"Documents/notes/todo.md",
|
||||
"dev/myproj/pkg/mod/thing.go",
|
||||
] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -491,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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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.3"
|
||||
version = "0.10.5"
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-mcp"
|
||||
version = "0.10.3"
|
||||
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.3", features = ["definitions"] }
|
||||
fff-query-parser = { path = "../fff-query-parser", default-features = false , version = "0.10.3" }
|
||||
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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+66
-14
@@ -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.
|
||||
@@ -182,11 +189,12 @@ pub(crate) struct Args {
|
||||
#[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,
|
||||
}
|
||||
@@ -335,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),
|
||||
@@ -353,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())
|
||||
@@ -363,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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -395,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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 ¶ms.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(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const REPO: &str = "dmtrKovalenko/fff.nvim";
|
||||
const REPO: &str = "dmtrKovalenko/fff";
|
||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
static UPDATE_NOTICE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-nvim"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
@@ -24,10 +24,10 @@ ahash = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Local crates
|
||||
fff = { package = "fff-search", path = "../fff-core", version = "0.10.3", 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.3", 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 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-python"
|
||||
version = "0.10.3"
|
||||
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.3", default-features = false }
|
||||
fff-query-parser = { path = "../fff-query-parser", version = "0.10.3", 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"] }
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-query-parser"
|
||||
version = "0.10.3"
|
||||
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"
|
||||
|
||||
@@ -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;
|
||||
@@ -498,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.
|
||||
@@ -1019,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,
|
||||
|
||||
+6
-4
@@ -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,
|
||||
|
||||
+6
-6
@@ -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
@@ -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": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
description = "fff.nvim";
|
||||
description = "fff";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
+2
-2
@@ -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' }
|
||||
|
||||
|
||||
+9
-9
@@ -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.10.1"
|
||||
PINNED_RELEASE_TAG="v0.10.5"
|
||||
|
||||
SHA256_X86_64_UNKNOWN_LINUX_MUSL="c17637c333afbbbdea4b03cf3e1573240c4147ae121756e363aafa3c9d0efb58"
|
||||
SHA256_AARCH64_UNKNOWN_LINUX_MUSL="2a25019101ee9373327dabd7ac1d4800638688e95b67bfb3bec441bf8d938f28"
|
||||
SHA256_X86_64_APPLE_DARWIN="d3c8d70d47ab80afa2287e1b4546b23f494bb4604b80e5a706a0af714ee25674"
|
||||
SHA256_AARCH64_APPLE_DARWIN="eff6660a9c48e3e1972d557c1003e057e5ff99d6039f5f810671f2123093fdfc"
|
||||
SHA256_X86_64_PC_WINDOWS_MSVC="e341b78464095c349b0c6b0a32b146fd217b542d973917b89645a5aa511640d8"
|
||||
SHA256_AARCH64_PC_WINDOWS_MSVC="c2fc1e55395f314a9493340f11f9b75501a6cd2c1e8fea646d343c7eaf3cd556"
|
||||
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
|
||||
|
||||
+47
-37
@@ -135,11 +135,17 @@ M.change_indexing_directory = function(new_path)
|
||||
return true
|
||||
end
|
||||
|
||||
M.ensure_initialized = function()
|
||||
if state.initialized then return fuzzy 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()
|
||||
local config = require('fff.conf').get()
|
||||
|
||||
-- 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)
|
||||
@@ -149,46 +155,50 @@ M.ensure_initialized = function()
|
||||
return fuzzy
|
||||
end
|
||||
|
||||
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)
|
||||
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 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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
return fuzzy
|
||||
-- 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
|
||||
|
||||
state.file_picker_initialized = true
|
||||
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',
|
||||
})
|
||||
|
||||
return fuzzy
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+7
-1
@@ -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
|
||||
|
||||
Generated
-4246
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -58,17 +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",
|
||||
"@ff-labs/fff-bin-android-arm64": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"typescript": "^5.0.0"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
@@ -60,19 +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",
|
||||
"@ff-labs/fff-bin-android-arm64": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"@types/node": "^22.0.0"
|
||||
"@types/node": "^22.0.0",
|
||||
"tsup": "^8.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -39,10 +39,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
binaryExists,
|
||||
findBinary,
|
||||
} from "./binary.js";
|
||||
export { binaryExists, findBinary } from "./binary.js";
|
||||
export { closeLibrary } from "./ffi.js";
|
||||
export type {
|
||||
DbHealth,
|
||||
|
||||
@@ -2,13 +2,13 @@ import { after, before, describe, it } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileFinder } from "../dist/src/index.js";
|
||||
import { FileFinder } from "../dist/index.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
const normalizePath = (p) => p.replace(/\\/g, "/");
|
||||
|
||||
/** @type {import("../dist/src/finder.js").FileFinder | null} */
|
||||
/** @type {import("../dist/finder.js").FileFinder | null} */
|
||||
let finder = null;
|
||||
|
||||
describe("fff-node", { concurrency: 1 }, () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { existsSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileFinder } from "../dist/src/index.js";
|
||||
import { FileFinder } from "../dist/index.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
|
||||
@@ -15,7 +15,7 @@ import { mkdtempSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, sep } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { FileFinder } from "../dist/src/index.js";
|
||||
import { FileFinder } from "../dist/index.js";
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -33,7 +33,7 @@ async function waitFor(cond, timeoutMs = 10_000) {
|
||||
/** All events delivered to a batch-callback mock, flattened across calls. */
|
||||
const deliveredEvents = (fn) => fn.mock.calls.flatMap((call) => call.arguments[0]);
|
||||
|
||||
/** @type {import("../dist/src/finder.js").FileFinder | null} */
|
||||
/** @type {import("../dist/finder.js").FileFinder | null} */
|
||||
let finder = null;
|
||||
/** @type {string} */
|
||||
let baseDir = "";
|
||||
@@ -237,7 +237,7 @@ describe("fff-node watch", { concurrency: 1 }, () => {
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { FileFinder } from ${JSON.stringify(new URL("../dist/src/index.js", import.meta.url).href)};
|
||||
import { FileFinder } from ${JSON.stringify(new URL("../dist/index.js", import.meta.url).href)};
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "fff-watch-exit-"));
|
||||
writeFileSync(join(dir, "seed.txt"), "seed");
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# fff-search
|
||||
|
||||
Python bindings for [FFF (Fast File Finder)](https://github.com/dmtrKovalenko/fff.nvim), built with [PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/). Install with `pip install fff-search`; import as `fff`.
|
||||
Python bindings for [FFF (Fast File Finder)](https://github.com/dmtrKovalenko/fff), built with [PyO3](https://pyo3.rs/) and [Maturin](https://www.maturin.rs/). Install with `pip install fff-search`; import as `fff`.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fff-search"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
description = "Python bindings for FFF (Fast File Finder)"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@@ -15,8 +15,8 @@ dev = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/dmtrKovalenko/fff.nvim"
|
||||
Issues = "https://github.com/dmtrKovalenko/fff.nvim/issues"
|
||||
Repository = "https://github.com/dmtrKovalenko/fff"
|
||||
Issues = "https://github.com/dmtrKovalenko/fff/issues"
|
||||
|
||||
[build-system]
|
||||
requires = ["maturin>=1.0,<2.0"]
|
||||
|
||||
@@ -50,7 +50,7 @@ class FileFinder(_FileFinder):
|
||||
return True
|
||||
|
||||
|
||||
__version__ = "0.10.3"
|
||||
__version__ = "0.10.5"
|
||||
|
||||
__all__ = [
|
||||
"FFFException",
|
||||
|
||||
Generated
+1
-1
@@ -34,7 +34,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-search"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
source = { editable = "." }
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Generated
+4608
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": ["fff-bun", "fff-node", "pi-fff"],
|
||||
"scripts": {
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"lint": "oxlint .",
|
||||
"check": "oxfmt . && oxlint --fix .",
|
||||
"check:ci": "oxfmt --check . && oxlint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"oxfmt": "0.63.0",
|
||||
"oxlint": "1.78.0"
|
||||
}
|
||||
}
|
||||
+61
-15
@@ -1,6 +1,6 @@
|
||||
# @ff-labs/pi-fff
|
||||
|
||||
A [pi](https://github.com/badlogic/pi-mono) extension that replaces the built-in `find` and `grep` tools with [FFF](https://github.com/dmtrKovalenko/fff.nvim) — a Rust-native, SIMD-accelerated file finder with built-in memory.
|
||||
A [pi](https://github.com/badlogic/pi-mono) extension that replaces the built-in `find` and `grep` tools with [FFF](https://github.com/dmtrKovalenko/fff) — a Rust-native, SIMD-accelerated file finder with built-in memory.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -44,20 +44,20 @@ pi install -l npm:@ff-labs/pi-fff
|
||||
**Via git:**
|
||||
|
||||
```bash
|
||||
pi install git:github.com/dmtrKovalenko/fff.nvim
|
||||
pi install git:github.com/dmtrKovalenko/fff
|
||||
```
|
||||
|
||||
Pin to a release:
|
||||
|
||||
```bash
|
||||
pi install git:github.com/dmtrKovalenko/fff.nvim@v0.3.0
|
||||
pi install git:github.com/dmtrKovalenko/fff@v0.3.0
|
||||
```
|
||||
|
||||
### Local development / manual install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dmtrKovalenko/fff.nvim.git
|
||||
cd fff.nvim/packages/pi-fff
|
||||
git clone https://github.com/dmtrKovalenko/fff.git
|
||||
cd fff/packages/pi-fff
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -65,14 +65,14 @@ Then add to your pi `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"extensions": ["/path/to/fff.nvim/packages/pi-fff/src/index.ts"]
|
||||
"extensions": ["/path/to/fff/packages/pi-fff/src/index.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
Or test directly:
|
||||
|
||||
```bash
|
||||
pi -e /path/to/fff.nvim/packages/pi-fff/src/index.ts
|
||||
pi -e /path/to/fff/packages/pi-fff/src/index.ts
|
||||
```
|
||||
|
||||
This extension registers FFF-powered tools (`fffind`, `ffgrep`, `fff-multi-grep`) alongside pi's built-in tools.
|
||||
@@ -116,7 +116,7 @@ Parameters:
|
||||
|
||||
- `/fff-health` — show FFF status (indexed files, git info, frecency/history DB status)
|
||||
- `/fff-rescan` — trigger a file rescan
|
||||
- `/fff-mode <mode>` — switch mode (tool name change requires restart)
|
||||
- `/fff-mode <mode>` — switch mode (tool name changes require `/reload`)
|
||||
|
||||
## Modes
|
||||
|
||||
@@ -124,24 +124,70 @@ Parameters:
|
||||
- `tools-only`: additional tools only; keep pi's default `@` autocomplete
|
||||
- `override`: replaces pi's built-in `find`, `grep` and adds `multi_grep` + FFF-backed `@` autocomplete
|
||||
|
||||
Mode precedence:
|
||||
Startup mode precedence:
|
||||
1. `--fff-mode <mode>` CLI flag
|
||||
2. `PI_FFF_MODE=<mode>` environment variable
|
||||
3. default (`tools-and-ui`)
|
||||
3. `mode` in the global config file
|
||||
4. default (`tools-and-ui`)
|
||||
|
||||
When a session resumes, its most recent `/fff-mode` selection takes precedence over the startup resolution above. Switching to or from `override` takes effect after `/reload`, when the tools are registered again.
|
||||
|
||||
## Configuration
|
||||
|
||||
For persistent global configuration, create `pi-fff.json` in pi's agent directory (`~/.pi/agent/pi-fff.json` by default; `PI_CODING_AGENT_DIR` is respected):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dmtrKovalenko/fff/main/packages/pi-fff/pi-fff.schema.json",
|
||||
"mode": "override",
|
||||
"frecencyDbPath": "/path/to/frecency",
|
||||
"historyDbPath": "/path/to/history",
|
||||
"enableFsRootScanning": false,
|
||||
"enableHomeDirScanning": true
|
||||
}
|
||||
```
|
||||
|
||||
All fields are optional:
|
||||
|
||||
| Field | Type | Default |
|
||||
|---|---|---|
|
||||
| `$schema` | non-empty string | none |
|
||||
| `mode` | `tools-and-ui`, `tools-only`, or `override` | `tools-and-ui` |
|
||||
| `frecencyDbPath` | non-empty string | See [Data](#data) |
|
||||
| `historyDbPath` | non-empty string | See [Data](#data) |
|
||||
| `enableFsRootScanning` | boolean | `false` |
|
||||
| `enableHomeDirScanning` | boolean | `true` |
|
||||
|
||||
CLI flags take precedence over environment variables, which take precedence over this file. A missing file is ignored. Malformed JSON, unknown fields, and invalid values stop the extension from loading and report the file path and error. `/fff-mode` changes the current session; it does not edit this file.
|
||||
|
||||
The file is global only. Project-level config cannot safely control tool names because pi decides which tools an extension registers before project configuration can be trusted.
|
||||
|
||||
## Flags
|
||||
|
||||
- `--fff-mode <mode>` — set mode (see above)
|
||||
- `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env)
|
||||
- `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env)
|
||||
- `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env). Optional; see [Data](#data) for the default.
|
||||
- `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env). Optional; see [Data](#data) for the default.
|
||||
- `--fff-enable-root-scan` — allow indexing when launched from `/` (also: `FFF_ENABLE_ROOT_SCAN=1` env). FFF refuses to init at the filesystem root by default.
|
||||
- `--fff-enable-home-scan` — index the home directory when launched from `$HOME` (also: `FFF_ENABLE_HOME_SCAN` env). Enabled by default. Disable with `--fff-enable-home-scan=false` or `FFF_ENABLE_HOME_SCAN=0` if your `$HOME` contains huge trees (toolchains, kernel sources, build outputs) that make the background index run for a long time. When launched from `$HOME` with this enabled, pi shows a warning that the whole home tree is being indexed.
|
||||
|
||||
## Data
|
||||
|
||||
When database paths are provided, FFF stores:
|
||||
- frecency database — file access frequency/recency
|
||||
- history database — query-to-file selection history
|
||||
FFF uses two LMDB databases:
|
||||
- frecency database - file access frequency/recency, used to rank results
|
||||
- history database - query-to-file selection history
|
||||
|
||||
Each path is resolved independently, in this order:
|
||||
|
||||
1. CLI flag — `--fff-frecency-db` / `--fff-history-db`
|
||||
2. Env var — `FFF_FRECENCY_DB` / `FFF_HISTORY_DB`
|
||||
3. Global config — `frecencyDbPath` / `historyDbPath`
|
||||
4. An existing [fff.nvim](https://github.com/dmtrKovalenko/fff.nvim) database, so pi reuses the frecency you built up in your editor:
|
||||
- frecency: `$XDG_CACHE_HOME/nvim/fff_nvim`
|
||||
- history: `$XDG_DATA_HOME/nvim/fff_queries`
|
||||
- `XDG_CACHE_HOME` defaults to `~/.cache` and `XDG_DATA_HOME` to `~/.local/share`; on Windows both fall back under `%LOCALAPPDATA%\nvim-data`. Only directories count — a plain file at those paths is ignored.
|
||||
5. pi-local directory, created on demand — `$PI_CODING_AGENT_DIR/fff/{frecency,history}`, defaulting to `~/.pi/agent/fff/{frecency,history}`
|
||||
|
||||
The extension only reads these databases; it never records the agent's own searches into your Neovim history. If a database cannot be opened, the finder starts without persistence and pi shows a warning instead of failing.
|
||||
|
||||
No project files are uploaded anywhere by this extension. It runs locally and only uses the configured LLM through pi itself.
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
"src",
|
||||
"pi-fff.schema.json"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -49,6 +50,7 @@
|
||||
"@sinclair/typebox": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/dmtrKovalenko/fff/main/packages/pi-fff/pi-fff.schema.json",
|
||||
"title": "pi-fff configuration",
|
||||
"description": "Global startup configuration for the pi-fff extension.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Schema URL used by editors for validation and completion."
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["tools-and-ui", "tools-only", "override"],
|
||||
"default": "tools-and-ui",
|
||||
"description": "Controls which FFF tools and autocomplete integrations are enabled."
|
||||
},
|
||||
"frecencyDbPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Path to the frecency database."
|
||||
},
|
||||
"historyDbPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Path to the query history database."
|
||||
},
|
||||
"enableFsRootScanning": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Allows indexing when pi is launched from the filesystem root."
|
||||
},
|
||||
"enableHomeDirScanning": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Allows indexing when pi is launched from the home directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { FileFinderApi } from "@ff-labs/fff-node";
|
||||
import type { FilePickerFactory } from "./file-picker";
|
||||
import { HOME_DIR } from "./paths";
|
||||
import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export const MAX_AUX = 3;
|
||||
export const IDLE_TTL_MS = 5 * 60 * 1000;
|
||||
@@ -16,6 +16,7 @@ interface AuxPicker {
|
||||
export interface AuxOpts {
|
||||
enableFsRootScanning: boolean;
|
||||
enableHomeDirScanning?: boolean;
|
||||
pickers: FilePickerFactory;
|
||||
// Called before a newly spawned aux picker starts a scan that covers $HOME.
|
||||
onHomeDirScan?: (root: string) => void;
|
||||
}
|
||||
@@ -99,25 +100,13 @@ export class AuxFinderPool {
|
||||
this.opts.onHomeDirScan?.(root);
|
||||
}
|
||||
|
||||
const { FileFinder } = await loadSdk();
|
||||
// LMDB env can only be opened once per process; the main finder already
|
||||
// owns the frecency/history DBs. Aux finders are transient and run without
|
||||
// persistent scoring — see issue #700.
|
||||
const result = FileFinder.create({
|
||||
const finder = await this.opts.pickers.create({
|
||||
basePath: root,
|
||||
aiMode: true,
|
||||
enableHomeDirScanning,
|
||||
enableFsRootScanning: this.opts.enableFsRootScanning,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(`Failed to create aux file finder for ${root}: ${result.error}`);
|
||||
|
||||
await result.value.waitForScan(SCAN_TIMEOUT_MS);
|
||||
const entry: AuxPicker = {
|
||||
root,
|
||||
finder: result.value,
|
||||
lastUsed: Date.now(),
|
||||
};
|
||||
const entry: AuxPicker = { root, finder, lastUsed: Date.now() };
|
||||
this.entries.push(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { piDataDir } from "./paths";
|
||||
|
||||
export const CONFIG_FILE_NAME = "pi-fff.json";
|
||||
export const VALID_MODES = ["tools-and-ui", "tools-only", "override"] as const;
|
||||
|
||||
export type FffMode = (typeof VALID_MODES)[number];
|
||||
|
||||
export interface FffConfig {
|
||||
$schema?: string;
|
||||
mode?: FffMode;
|
||||
frecencyDbPath?: string;
|
||||
historyDbPath?: string;
|
||||
enableFsRootScanning?: boolean;
|
||||
enableHomeDirScanning?: boolean;
|
||||
}
|
||||
|
||||
const CONFIG_KEYS = new Set<keyof FffConfig>([
|
||||
"$schema",
|
||||
"mode",
|
||||
"frecencyDbPath",
|
||||
"historyDbPath",
|
||||
"enableFsRootScanning",
|
||||
"enableHomeDirScanning",
|
||||
]);
|
||||
|
||||
export function loadConfig(agentDir = piDataDir()): FffConfig {
|
||||
const configPath = join(agentDir, CONFIG_FILE_NAME);
|
||||
let contents: string;
|
||||
|
||||
try {
|
||||
contents = readFileSync(configPath, "utf8");
|
||||
} catch (error: unknown) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
||||
throw new Error(
|
||||
`Could not read pi-fff config at ${configPath}: ${errorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (error: unknown) {
|
||||
throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`);
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
throw invalidConfig(configPath, "expected a JSON object");
|
||||
}
|
||||
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (!CONFIG_KEYS.has(key as keyof FffConfig)) {
|
||||
throw invalidConfig(configPath, `unknown option "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.mode !== undefined && !VALID_MODES.includes(parsed.mode as FffMode)) {
|
||||
throw invalidConfig(configPath, `"mode" must be one of ${VALID_MODES.join(", ")}`);
|
||||
}
|
||||
|
||||
validateString(configPath, parsed, "$schema");
|
||||
validateString(configPath, parsed, "frecencyDbPath");
|
||||
validateString(configPath, parsed, "historyDbPath");
|
||||
validateBoolean(configPath, parsed, "enableFsRootScanning");
|
||||
validateBoolean(configPath, parsed, "enableHomeDirScanning");
|
||||
|
||||
return parsed as FffConfig;
|
||||
}
|
||||
|
||||
function invalidConfig(configPath: string, reason: string): Error {
|
||||
return new Error(`Invalid pi-fff config at ${configPath}: ${reason}`);
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function validateString(
|
||||
configPath: string,
|
||||
config: Record<string, unknown>,
|
||||
key: "$schema" | "frecencyDbPath" | "historyDbPath",
|
||||
): void {
|
||||
const value = config[key];
|
||||
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
|
||||
throw invalidConfig(configPath, `"${key}" must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateBoolean(
|
||||
configPath: string,
|
||||
config: Record<string, unknown>,
|
||||
key: "enableFsRootScanning" | "enableHomeDirScanning",
|
||||
): void {
|
||||
const value = config[key];
|
||||
if (value !== undefined && typeof value !== "boolean") {
|
||||
throw invalidConfig(configPath, `"${key}" must be a boolean`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { FileFinderApi, InitOptions, Result } from "@ff-labs/fff-node";
|
||||
import { type FileFinderStatic, loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export interface PickerOptions {
|
||||
basePath: string;
|
||||
enableHomeDirScanning?: boolean;
|
||||
enableFsRootScanning?: boolean;
|
||||
}
|
||||
|
||||
/** Opens every picker in this pi process — the cwd picker and the aux pickers —
|
||||
* on the same frecency/history databases. */
|
||||
export class FilePickerFactory {
|
||||
private dbDisabled = false;
|
||||
private readonly frecencyDbPath: string;
|
||||
private readonly historyDbPath: string;
|
||||
private readonly onDbFailure?: (error: string) => void;
|
||||
|
||||
constructor(opts: {
|
||||
frecencyDbPath: string;
|
||||
historyDbPath: string;
|
||||
onDbFailure?: (error: string) => void;
|
||||
}) {
|
||||
this.frecencyDbPath = opts.frecencyDbPath;
|
||||
this.historyDbPath = opts.historyDbPath;
|
||||
this.onDbFailure = opts.onDbFailure;
|
||||
}
|
||||
|
||||
/** True once the databases were given up on, so pickers open without them. */
|
||||
get databasesDisabled(): boolean {
|
||||
return this.dbDisabled;
|
||||
}
|
||||
|
||||
/** Opens a scanned, ready-to-use picker. Throws if it cannot be created. */
|
||||
async create(options: PickerOptions): Promise<FileFinderApi> {
|
||||
const { FileFinder } = await loadSdk();
|
||||
const result = this.openWithDbFallback(FileFinder, options);
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
`Failed to create FFF file picker for ${options.basePath}: ${result.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
// waitForScan() also resolves on timeout, so this bounds startup rather
|
||||
// than guaranteeing a complete index.
|
||||
await result.value.waitForScan(SCAN_TIMEOUT_MS);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
private openWithDbFallback(
|
||||
FileFinder: FileFinderStatic,
|
||||
options: PickerOptions,
|
||||
): Result<FileFinderApi> {
|
||||
const init: InitOptions = { ...options, aiMode: true };
|
||||
if (this.dbDisabled) return FileFinder.create(init);
|
||||
|
||||
const result = FileFinder.create({
|
||||
...init,
|
||||
frecencyDbPath: this.frecencyDbPath,
|
||||
historyDbPath: this.historyDbPath,
|
||||
});
|
||||
if (result.ok) return result;
|
||||
|
||||
// A failure here is usually transient (broken lock, corruption) and self-heals
|
||||
// on restart, so drop the databases instead of leaving pi without a picker
|
||||
const dbLess = FileFinder.create(init);
|
||||
if (!dbLess.ok) return result; // db error is the more useful one to report
|
||||
|
||||
this.dbDisabled = true;
|
||||
this.onDbFailure?.(result.error);
|
||||
return dbLess;
|
||||
}
|
||||
}
|
||||
+288
-156
@@ -6,7 +6,11 @@
|
||||
*/
|
||||
|
||||
import nodePath from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
ToolDefinition,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
type AutocompleteItem,
|
||||
type AutocompleteProvider,
|
||||
@@ -20,11 +24,12 @@ import type {
|
||||
MixedItem,
|
||||
SearchResult,
|
||||
} from "@ff-labs/fff-node";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Type, type TSchema } from "@sinclair/typebox";
|
||||
import { AuxFinderPool, routePathConstraint } from "./aux-finders";
|
||||
import { type FffMode, loadConfig, VALID_MODES } from "./config";
|
||||
import { FilePickerFactory } from "./file-picker";
|
||||
import { isHomeDir, resolveDbPaths } from "./paths";
|
||||
import { buildQuery } from "./query";
|
||||
import { isHomeDir } from "./paths";
|
||||
import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export { SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
@@ -34,6 +39,8 @@ export { SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
const DEFAULT_GREP_LIMIT = 20;
|
||||
const DEFAULT_FIND_LIMIT = 30;
|
||||
const GREP_PAGE_SIZE_MAX = 50;
|
||||
const GREP_CONTEXT_MAX = 20;
|
||||
const GREP_MAX_LINE_LENGTH = 500;
|
||||
const MENTION_MAX_RESULTS = 20;
|
||||
|
||||
@@ -43,11 +50,7 @@ const GREP_TIME_BUDGET_MS = 10_000;
|
||||
const HOME_SCAN_STATUS_KEY = "fff";
|
||||
const HOME_SCAN_POLL_MS = 1_000;
|
||||
const HOME_SCAN_DISABLE_HINT =
|
||||
"You can prevent home dir indexing with --fff-enable-home-scan=false (or FFF_ENABLE_HOME_SCAN=0).";
|
||||
|
||||
type FffMode = "tools-and-ui" | "tools-only" | "override";
|
||||
|
||||
const VALID_MODES: FffMode[] = ["tools-and-ui", "tools-only", "override"];
|
||||
"You can prevent home dir indexing with --fff-enable-home-scan=false, FFF_ENABLE_HOME_SCAN=0, or enableHomeDirScanning in pi-fff.json.";
|
||||
|
||||
interface ToolNames {
|
||||
grep: string;
|
||||
@@ -128,6 +131,13 @@ function truncateLine(line: string, max = GREP_MAX_LINE_LENGTH): string {
|
||||
return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}...`;
|
||||
}
|
||||
|
||||
// Clamp caller-supplied context to a non-negative bounded integer so a large
|
||||
// value cannot multiply output size past the model window.
|
||||
function clampContext(context: number | undefined): number {
|
||||
if (!context || context < 0) return 0;
|
||||
return Math.min(Math.floor(context), GREP_CONTEXT_MAX);
|
||||
}
|
||||
|
||||
const HOT_FRECENCY = 25;
|
||||
const WARM_FRECENCY = 20;
|
||||
|
||||
@@ -153,16 +163,7 @@ export function fffFileAnnotation(item: {
|
||||
return "";
|
||||
}
|
||||
|
||||
// fff-core native definition classifier (byte-level scanner in Rust) is enabled
|
||||
// via GrepOptions.classifyDefinitions. Each GrepMatch carries isDefinition for
|
||||
// downstream consumers; pi-fff does NOT use it to re-sort.
|
||||
//
|
||||
// Ordering policy: NO CUSTOM SORTING. The engine already returns items in
|
||||
// frecency order (most-accessed files first). pi-fff only groups consecutive
|
||||
// matches into per-file blocks and preserves whatever order the engine
|
||||
// provided — inside a file we keep matches in source-line order because the
|
||||
// engine emits them that way.
|
||||
|
||||
// DO NOT ATTEMPT TO RESORT OUTPUT HERE IT ONLY CONFUSES MODELS
|
||||
function formatGrepOutput(result: GrepResult): string {
|
||||
if (result.items.length === 0) return "No matches found";
|
||||
|
||||
@@ -170,7 +171,6 @@ function formatGrepOutput(result: GrepResult): string {
|
||||
// This preserves native frecency ordering across files without re-sorting.
|
||||
const lines: string[] = [];
|
||||
let currentFile = "";
|
||||
let shown = 0;
|
||||
|
||||
for (const match of result.items) {
|
||||
if (match.relativePath !== currentFile) {
|
||||
@@ -185,7 +185,6 @@ function formatGrepOutput(result: GrepResult): string {
|
||||
});
|
||||
|
||||
lines.push(` ${match.lineNumber}: ${truncateLine(match.lineContent)}`);
|
||||
shown++;
|
||||
|
||||
match.contextAfter?.forEach((line: string, i: number) => {
|
||||
const lineNum = match.lineNumber + 1 + i;
|
||||
@@ -301,57 +300,95 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
let finderPromise: Promise<FileFinderApi> | null = null;
|
||||
let activeCwd = process.cwd();
|
||||
|
||||
// Mode resolution: flag > env > default
|
||||
let currentMode: FffMode =
|
||||
(pi.getFlag("fff-mode") as FffMode) ??
|
||||
(process.env.PI_FFF_MODE as FffMode) ??
|
||||
"tools-and-ui";
|
||||
const config = loadConfig();
|
||||
|
||||
const toolNames = resolveToolNames(currentMode);
|
||||
|
||||
// DB path resolution: flag > env > undefined (use fff-node defaults)
|
||||
const frecencyDbPath =
|
||||
(pi.getFlag("fff-frecency-db") as string | undefined) ??
|
||||
process.env.FFF_FRECENCY_DB ??
|
||||
undefined;
|
||||
const historyDbPath =
|
||||
(pi.getFlag("fff-history-db") as string | undefined) ??
|
||||
process.env.FFF_HISTORY_DB ??
|
||||
undefined;
|
||||
|
||||
// flag (boolean) > env ("1"/"true", or "0"/"false") > default.
|
||||
function resolveBoolOpt(
|
||||
// All startup options use the same flag > env > file > fallback order.
|
||||
function getConfigValue<T>(
|
||||
flagName: string,
|
||||
envName: string,
|
||||
fallback = false,
|
||||
): boolean {
|
||||
const flag = pi.getFlag(flagName);
|
||||
if (typeof flag === "boolean") return flag;
|
||||
if (typeof flag === "string") return flag === "true" || flag === "1";
|
||||
const env = process.env[envName];
|
||||
if (env === "1" || env === "true") return true;
|
||||
if (env === "0" || env === "false") return false;
|
||||
return fallback;
|
||||
}
|
||||
// Root scanning opt-in: FFF refuses to init at / unless this is set.
|
||||
const enableFsRootScanning = resolveBoolOpt(
|
||||
"fff-enable-root-scan",
|
||||
"FFF_ENABLE_ROOT_SCAN",
|
||||
);
|
||||
// Home dir scanning is on by default (launching pi from $HOME is a normal
|
||||
// flow), but configurable so users with huge $HOME trees can opt out.
|
||||
const enableHomeDirScanning = resolveBoolOpt(
|
||||
"fff-enable-home-scan",
|
||||
"FFF_ENABLE_HOME_SCAN",
|
||||
true,
|
||||
);
|
||||
fileValue: T | undefined,
|
||||
fallback: T,
|
||||
parse: (value: unknown) => T | undefined = (value) => value as T,
|
||||
): T {
|
||||
const flagValue = pi.getFlag(flagName);
|
||||
if (flagValue !== undefined) {
|
||||
const value = parse(flagValue);
|
||||
if (value !== undefined) return value;
|
||||
}
|
||||
|
||||
function getMode(): FffMode {
|
||||
return currentMode;
|
||||
const envValue = process.env[envName];
|
||||
if (envValue !== undefined) {
|
||||
const value = parse(envValue);
|
||||
if (value !== undefined) return value;
|
||||
}
|
||||
|
||||
return fileValue ?? fallback;
|
||||
}
|
||||
|
||||
function parseBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (value === "1" || value === "true") return true;
|
||||
if (value === "0" || value === "false") return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseMode(value: unknown): FffMode | undefined {
|
||||
return typeof value === "string" && VALID_MODES.includes(value as FffMode)
|
||||
? (value as FffMode)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
let currentMode: FffMode = "tools-and-ui";
|
||||
let toolNames = resolveToolNames(currentMode);
|
||||
let resolvedDbPaths: ReturnType<typeof resolveDbPaths>;
|
||||
let enableFsRootScanning = false;
|
||||
let enableHomeDirScanning = true;
|
||||
|
||||
function setMode(mode: FffMode): void {
|
||||
currentMode = mode;
|
||||
toolNames = resolveToolNames(mode);
|
||||
}
|
||||
|
||||
function resolveStartupConfig(): void {
|
||||
setMode(
|
||||
getConfigValue("fff-mode", "PI_FFF_MODE", config.mode, "tools-and-ui", parseMode),
|
||||
);
|
||||
resolvedDbPaths = resolveDbPaths({
|
||||
frecency: getConfigValue(
|
||||
"fff-frecency-db",
|
||||
"FFF_FRECENCY_DB",
|
||||
config.frecencyDbPath,
|
||||
undefined,
|
||||
),
|
||||
history: getConfigValue(
|
||||
"fff-history-db",
|
||||
"FFF_HISTORY_DB",
|
||||
config.historyDbPath,
|
||||
undefined,
|
||||
),
|
||||
});
|
||||
|
||||
// Root scanning opt-in: FFF refuses to init at / unless this is set.
|
||||
enableFsRootScanning = getConfigValue(
|
||||
"fff-enable-root-scan",
|
||||
"FFF_ENABLE_ROOT_SCAN",
|
||||
config.enableFsRootScanning,
|
||||
false,
|
||||
parseBoolean,
|
||||
);
|
||||
// Home dir scanning is on by default (launching pi from $HOME is a normal
|
||||
// flow), but configurable so users with huge $HOME trees can opt out.
|
||||
enableHomeDirScanning = getConfigValue(
|
||||
"fff-enable-home-scan",
|
||||
"FFF_ENABLE_HOME_SCAN",
|
||||
config.enableHomeDirScanning,
|
||||
true,
|
||||
parseBoolean,
|
||||
);
|
||||
}
|
||||
|
||||
function getMode(): FffMode {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
function shouldEnableMentions(): boolean {
|
||||
@@ -375,11 +412,28 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
);
|
||||
}
|
||||
|
||||
let auxPool = new AuxFinderPool({
|
||||
enableFsRootScanning,
|
||||
enableHomeDirScanning,
|
||||
onHomeDirScan: warnHomeDirScan,
|
||||
});
|
||||
let pickers: FilePickerFactory | null = null;
|
||||
let auxPool: AuxFinderPool | null = null;
|
||||
|
||||
function initializeFinderFactories(): void {
|
||||
if (pickers) return;
|
||||
|
||||
pickers = new FilePickerFactory({
|
||||
frecencyDbPath: resolvedDbPaths.frecency,
|
||||
historyDbPath: resolvedDbPaths.history,
|
||||
onDbFailure: (error) =>
|
||||
uiCtx?.ui.notify(
|
||||
`(fff): Failed to open frecency/history database (${error}). Continuing without frecency persistence.`,
|
||||
"error",
|
||||
),
|
||||
});
|
||||
auxPool = new AuxFinderPool({
|
||||
enableFsRootScanning,
|
||||
enableHomeDirScanning,
|
||||
onHomeDirScan: warnHomeDirScan,
|
||||
pickers,
|
||||
});
|
||||
}
|
||||
|
||||
// in case cwd changes we need to figure this out
|
||||
function ensureFinder(cwd: string): Promise<FileFinderApi> {
|
||||
@@ -395,22 +449,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
finderCwd = null;
|
||||
}
|
||||
|
||||
const { FileFinder } = await loadSdk();
|
||||
const result = FileFinder.create({
|
||||
// if the dbs can't be opened the factory falls back to a db-less picker,
|
||||
// e.g. when some other process corrupts the lock
|
||||
if (!pickers) throw new Error("FFF picker factory is not initialized");
|
||||
mainFinder = await pickers.create({
|
||||
basePath: cwd,
|
||||
frecencyDbPath,
|
||||
historyDbPath,
|
||||
aiMode: true,
|
||||
enableHomeDirScanning,
|
||||
enableFsRootScanning,
|
||||
});
|
||||
|
||||
if (!result.ok)
|
||||
throw new Error(`Failed to create FFF file finder: ${result.error}`);
|
||||
|
||||
mainFinder = result.value;
|
||||
finderCwd = cwd;
|
||||
await mainFinder.waitForScan(SCAN_TIMEOUT_MS);
|
||||
return mainFinder;
|
||||
})().finally(() => {
|
||||
finderPromise = null;
|
||||
@@ -459,9 +506,9 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
finderCwd = null;
|
||||
}
|
||||
|
||||
if (auxPool) {
|
||||
auxPool.destroy();
|
||||
}
|
||||
auxPool?.destroy();
|
||||
auxPool = null;
|
||||
pickers = null;
|
||||
}
|
||||
|
||||
async function resolveFinderForPath(
|
||||
@@ -471,6 +518,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
): Promise<{ finder: FileFinderApi; query: string; root: string } | null> {
|
||||
const route = routePathConstraint(pathParam, activeCwd);
|
||||
if (!route) return null;
|
||||
if (!auxPool) throw new Error("FFF auxiliary finder pool is not initialized");
|
||||
const aux = await auxPool.acquire(route.root);
|
||||
// A broader covering picker may have been reused; rebase the suffix so the
|
||||
// constraint stays relative to the picker's actual root.
|
||||
@@ -551,6 +599,45 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
});
|
||||
}
|
||||
|
||||
type PendingToolDefinition<
|
||||
TParams extends TSchema,
|
||||
TDetails = unknown,
|
||||
TState = any,
|
||||
> = Omit<
|
||||
ToolDefinition<TParams, TDetails, TState>,
|
||||
"name" | "label" | "promptGuidelines"
|
||||
> & {
|
||||
promptGuidelines?: (names: ToolNames) => string[];
|
||||
};
|
||||
|
||||
const pendingTools: (() => string)[] = [];
|
||||
let toolsRegistered = false;
|
||||
|
||||
function queueTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
||||
resolveName: () => string,
|
||||
definition: PendingToolDefinition<TParams, TDetails, TState>,
|
||||
): void {
|
||||
pendingTools.push(() => {
|
||||
const { promptGuidelines, ...tool } = definition;
|
||||
const resolvedName = resolveName();
|
||||
pi.registerTool({
|
||||
...tool,
|
||||
name: resolvedName,
|
||||
label: resolvedName,
|
||||
promptGuidelines: promptGuidelines?.(toolNames),
|
||||
});
|
||||
return resolvedName;
|
||||
});
|
||||
}
|
||||
|
||||
function registerPendingTools(): void {
|
||||
if (toolsRegistered) return;
|
||||
|
||||
const registeredNames = pendingTools.map((register) => register());
|
||||
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...registeredNames])]);
|
||||
toolsRegistered = true;
|
||||
}
|
||||
|
||||
// --- Flags / lifecycle ---
|
||||
|
||||
pi.registerFlag("fff-mode", {
|
||||
@@ -580,34 +667,48 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
type: "boolean",
|
||||
});
|
||||
|
||||
function reportInitFailure(ctx: ExtensionContext, error: unknown): void {
|
||||
ctx.ui.notify(
|
||||
`FFF init failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
|
||||
function prepareSession(ctx: ExtensionContext): void {
|
||||
activeCwd = ctx.cwd;
|
||||
uiCtx = ctx;
|
||||
if (toolsRegistered) return;
|
||||
|
||||
// Pi populates extension flag values after loading extensions.
|
||||
resolveStartupConfig();
|
||||
|
||||
// Restore persisted mode before registering tools so a saved override
|
||||
// can safely change their names after /reload or session resume.
|
||||
const entries = ctx.sessionManager?.getEntries();
|
||||
if (entries) {
|
||||
const modeEntry = [...entries]
|
||||
.reverse()
|
||||
.find(
|
||||
(e: { type: string; customType?: string }) =>
|
||||
e.type === "custom" && e.customType === "fff-mode",
|
||||
);
|
||||
if (
|
||||
modeEntry &&
|
||||
typeof (modeEntry as any).data?.mode === "string" &&
|
||||
VALID_MODES.includes((modeEntry as any).data.mode as FffMode)
|
||||
) {
|
||||
const restored = (modeEntry as any).data.mode as FffMode;
|
||||
if (restored !== currentMode) setMode(restored);
|
||||
}
|
||||
}
|
||||
|
||||
initializeFinderFactories();
|
||||
registerPendingTools();
|
||||
}
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
try {
|
||||
activeCwd = ctx.cwd;
|
||||
uiCtx = ctx as unknown as typeof uiCtx;
|
||||
|
||||
// Restore persisted mode from session entries. This handles session
|
||||
// resume after process restart where env vars are lost, and ensures
|
||||
// the env var is set for the next /reload in the same session.
|
||||
const entries = ctx.sessionManager?.getEntries();
|
||||
if (entries) {
|
||||
const modeEntry = [...entries]
|
||||
.reverse()
|
||||
.find(
|
||||
(e: { type: string; customType?: string }) =>
|
||||
e.type === "custom" && e.customType === "fff-mode",
|
||||
);
|
||||
if (
|
||||
modeEntry &&
|
||||
typeof (modeEntry as any).data?.mode === "string" &&
|
||||
VALID_MODES.includes((modeEntry as any).data.mode as FffMode)
|
||||
) {
|
||||
const restored = (modeEntry as any).data.mode as FffMode;
|
||||
if (restored !== currentMode) {
|
||||
currentMode = restored;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareSession(ctx);
|
||||
registerAutocompleteProvider(ctx);
|
||||
await ensureFinder(activeCwd);
|
||||
|
||||
@@ -625,11 +726,19 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
// waitForScan() also resolves on timeout, so poll until the scan really
|
||||
// settles before clearing the footer.
|
||||
if (atHome) trackHomeScanStatus();
|
||||
} catch (e: unknown) {
|
||||
ctx.ui.notify(
|
||||
`FFF init failed: ${e instanceof Error ? e.message : String(e)}`,
|
||||
"error",
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
reportInitFailure(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// SDK callers can prompt without binding session_start. Prepare on the first
|
||||
// agent turn as a fallback so the tools still reach that turn's tool set.
|
||||
pi.on("before_agent_start", (_event, ctx) => {
|
||||
if (toolsRegistered) return;
|
||||
try {
|
||||
prepareSession(ctx);
|
||||
} catch (error: unknown) {
|
||||
reportInitFailure(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -691,7 +800,9 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
}),
|
||||
),
|
||||
context: Type.Optional(
|
||||
Type.Number({ description: "Context lines before+after each match" }),
|
||||
Type.Number({
|
||||
description: `Context lines before+after each match (0-${GREP_CONTEXT_MAX})`,
|
||||
}),
|
||||
),
|
||||
limit: Type.Optional(
|
||||
Type.Number({
|
||||
@@ -703,16 +814,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
),
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: toolNames.grep,
|
||||
label: toolNames.grep,
|
||||
queueTool(() => toolNames.grep, {
|
||||
description: `Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${DEFAULT_GREP_LIMIT}.`,
|
||||
promptSnippet: "Grep contents",
|
||||
promptGuidelines: [
|
||||
`${toolNames.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
||||
`${toolNames.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
||||
`${toolNames.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
||||
`${toolNames.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
||||
promptGuidelines: (names) => [
|
||||
`${names.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
||||
`${names.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
||||
`${names.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
||||
`${names.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
||||
],
|
||||
parameters: grepSchema,
|
||||
|
||||
@@ -724,6 +833,10 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
|
||||
const picker = aux ? aux.finder : await ensureFinder(activeCwd);
|
||||
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
||||
// pageSize caps TOTAL matches across all files; maxMatchesPerFile alone
|
||||
// only caps per-file, so limit=5 could still return a full SDK page.
|
||||
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
||||
const context = clampContext(params.context);
|
||||
const query = aux
|
||||
? aux.query
|
||||
: buildQuery(params.path, pattern, params.exclude, activeCwd);
|
||||
@@ -771,10 +884,11 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
const grepResult = picker.grep(query, {
|
||||
mode,
|
||||
smartCase,
|
||||
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
||||
maxMatchesPerFile: pageSize,
|
||||
pageSize,
|
||||
cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
|
||||
beforeContext: params.context ?? 0,
|
||||
afterContext: params.context ?? 0,
|
||||
beforeContext: context,
|
||||
afterContext: context,
|
||||
classifyDefinitions: true,
|
||||
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
||||
});
|
||||
@@ -803,7 +917,8 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
const fuzzy = picker.grep(fuzzyQuery, {
|
||||
mode: "fuzzy",
|
||||
smartCase,
|
||||
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
||||
maxMatchesPerFile: pageSize,
|
||||
pageSize,
|
||||
cursor: null,
|
||||
beforeContext: 0,
|
||||
afterContext: 0,
|
||||
@@ -888,18 +1003,16 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
),
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: toolNames.find,
|
||||
label: toolNames.find,
|
||||
queueTool(() => toolNames.find, {
|
||||
description: `Fuzzy path search and glob search. Matches against the whole repo-relative path, not just the filename. Frecency-ranked, git-aware. Multi-word = narrower (AND). Default limit ${DEFAULT_FIND_LIMIT}.`,
|
||||
promptSnippet: "Find files by path or glob",
|
||||
promptGuidelines: [
|
||||
`${toolNames.find}: matches the WHOLE path, not just the filename — \`profile\` hits \`chrome/browser/profiles/x.cc\` too.`,
|
||||
`${toolNames.find}: keep queries to 1-2 terms; extra words narrow.`,
|
||||
`${toolNames.find}: use for paths, not content. Use ${toolNames.grep} for content.`,
|
||||
`${toolNames.find}: for exact path matches use a glob in \`path\` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.`,
|
||||
`${toolNames.find}: to list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.`,
|
||||
`${toolNames.find}: use exclude: 'test/,*.min.js' to cut noise in large repos.`,
|
||||
promptGuidelines: (names) => [
|
||||
`${names.find}: matches the WHOLE path, not just the filename — \`profile\` hits \`chrome/browser/profiles/x.cc\` too.`,
|
||||
`${names.find}: keep queries to 1-2 terms; extra words narrow.`,
|
||||
`${names.find}: use for paths, not content. Use ${names.grep} for content.`,
|
||||
`${names.find}: for exact path matches use a glob in \`path\` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.`,
|
||||
`${names.find}: to list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.`,
|
||||
`${names.find}: use exclude: 'test/,*.min.js' to cut noise in large repos.`,
|
||||
],
|
||||
parameters: findSchema,
|
||||
|
||||
@@ -908,10 +1021,12 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
|
||||
// if resumed we use the same picker as before
|
||||
const resumed = params.cursor ? getFindCursor(params.cursor) : undefined;
|
||||
const pool = auxPool;
|
||||
if (!pool) throw new Error("FFF auxiliary finder pool is not initialized");
|
||||
const aux = resumed
|
||||
? resumed.auxRoot
|
||||
? {
|
||||
finder: (await auxPool.acquire(resumed.auxRoot, { exact: true })).finder,
|
||||
finder: (await pool.acquire(resumed.auxRoot, { exact: true })).finder,
|
||||
root: resumed.auxRoot,
|
||||
}
|
||||
: null
|
||||
@@ -1015,7 +1130,11 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
constraints: Type.Optional(
|
||||
Type.String({ description: "File filter, e.g. '*.{ts,tsx} !test/'" }),
|
||||
),
|
||||
context: Type.Optional(Type.Number({ description: "Context lines before+after" })),
|
||||
context: Type.Optional(
|
||||
Type.Number({
|
||||
description: `Context lines before+after (0-${GREP_CONTEXT_MAX})`,
|
||||
}),
|
||||
),
|
||||
limit: Type.Optional(
|
||||
Type.Number({
|
||||
description: `Max matches (default ${DEFAULT_GREP_LIMIT})`,
|
||||
@@ -1024,16 +1143,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: toolNames.multiGrep,
|
||||
label: toolNames.multiGrep,
|
||||
queueTool(() => toolNames.multiGrep, {
|
||||
description:
|
||||
"Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",
|
||||
promptSnippet: "Multi-pattern OR content search",
|
||||
promptGuidelines: [
|
||||
`${toolNames.multiGrep}: use when searching for several identifiers at once.`,
|
||||
`${toolNames.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
||||
`${toolNames.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
||||
promptGuidelines: (names) => [
|
||||
`${names.multiGrep}: use when searching for several identifiers at once.`,
|
||||
`${names.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
||||
`${names.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
||||
],
|
||||
parameters: multiGrepSchema,
|
||||
|
||||
@@ -1044,15 +1161,18 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
|
||||
const f = await ensureFinder(activeCwd);
|
||||
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
||||
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
||||
const context = clampContext(params.context);
|
||||
|
||||
const grepResult = f.multiGrep({
|
||||
patterns: params.patterns,
|
||||
constraints: params.constraints,
|
||||
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
||||
maxMatchesPerFile: pageSize,
|
||||
pageSize,
|
||||
smartCase: true,
|
||||
cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
|
||||
beforeContext: params.context ?? 0,
|
||||
afterContext: params.context ?? 0,
|
||||
beforeContext: context,
|
||||
afterContext: context,
|
||||
});
|
||||
|
||||
if (!grepResult.ok) throw new Error(grepResult.error);
|
||||
@@ -1105,6 +1225,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
pi.registerCommand("fff-mode", {
|
||||
description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
|
||||
handler: async (args, ctx) => {
|
||||
if (!toolsRegistered) {
|
||||
try {
|
||||
prepareSession(ctx);
|
||||
} catch (error: unknown) {
|
||||
reportInitFailure(ctx, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const arg = (args || "").trim();
|
||||
|
||||
// No args - show current mode
|
||||
@@ -1123,15 +1252,18 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
|
||||
const newMode = arg as FffMode;
|
||||
const oldMode = getMode();
|
||||
setMode(newMode);
|
||||
|
||||
pi.appendEntry("fff-mode", { mode: newMode });
|
||||
|
||||
const note =
|
||||
(oldMode === "override") !== (newMode === "override")
|
||||
? " (tool name change requires /reload)"
|
||||
: "";
|
||||
ctx.ui.notify(`Mode changed: '${oldMode}' → '${newMode}'${note}`, "info");
|
||||
if ((oldMode === "override") !== (newMode === "override")) {
|
||||
ctx.ui.notify(
|
||||
`Mode '${newMode}' saved. Run /reload to apply the tool name change.`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setMode(newMode);
|
||||
ctx.ui.notify(`Mode changed: '${oldMode}' → '${newMode}'`, "info");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// Resolved once per process: os.homedir() hits the env/passwd on every call.
|
||||
export const HOME_DIR = path.resolve(os.homedir());
|
||||
|
||||
// fff.nvim db dir names (`frecency.db_path` / `history.db_path` in lua/fff/conf.lua).
|
||||
const NVIM_FRECENCY_DIR = "fff_nvim";
|
||||
const NVIM_HISTORY_DIR = "fff_queries";
|
||||
|
||||
export interface DbPaths {
|
||||
frecency: string;
|
||||
history: string;
|
||||
}
|
||||
|
||||
export function isHomeDir(dir: string): boolean {
|
||||
return path.resolve(dir) === HOME_DIR;
|
||||
}
|
||||
|
||||
// Resolution order: explicit override > existing fff.nvim db > pi-local data dir.
|
||||
// Reusing the nvim db lets pi rank files by the frecency the user built in their editor.
|
||||
export function resolveDbPaths(overrides: {
|
||||
frecency?: string;
|
||||
history?: string;
|
||||
}): DbPaths {
|
||||
return {
|
||||
frecency:
|
||||
overrides.frecency ??
|
||||
existingDir(nvimCacheDir(), NVIM_FRECENCY_DIR) ??
|
||||
path.join(piDataDir(), "fff", "frecency"),
|
||||
history:
|
||||
overrides.history ??
|
||||
existingDir(nvimDataDir(), NVIM_HISTORY_DIR) ??
|
||||
path.join(piDataDir(), "fff", "history"),
|
||||
};
|
||||
}
|
||||
|
||||
function nvimCacheDir(): string {
|
||||
const xdg = process.env.XDG_CACHE_HOME;
|
||||
if (xdg) return path.join(xdg, "nvim");
|
||||
if (process.platform === "win32" && process.env.LOCALAPPDATA)
|
||||
return path.join(process.env.LOCALAPPDATA, "nvim-data", "cache");
|
||||
return path.join(HOME_DIR, ".cache", "nvim");
|
||||
}
|
||||
|
||||
function nvimDataDir(): string {
|
||||
const xdg = process.env.XDG_DATA_HOME;
|
||||
if (xdg) return path.join(xdg, "nvim");
|
||||
if (process.platform === "win32" && process.env.LOCALAPPDATA)
|
||||
return path.join(process.env.LOCALAPPDATA, "nvim-data");
|
||||
return path.join(HOME_DIR, ".local", "share", "nvim");
|
||||
}
|
||||
|
||||
export function piDataDir(): string {
|
||||
return process.env.PI_CODING_AGENT_DIR ?? path.join(HOME_DIR, ".pi", "agent");
|
||||
}
|
||||
|
||||
// LMDB environments are directories, so a stray file at the same path is not a db.
|
||||
function existingDir(parent: string, name: string): string | undefined {
|
||||
const candidate = path.join(parent, name);
|
||||
try {
|
||||
return fs.statSync(candidate).isDirectory() ? candidate : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,21 @@ function detectRuntime(): "bun" | "node" {
|
||||
export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
|
||||
if (sdkPromise) return sdkPromise;
|
||||
|
||||
// Pi reloads extension modules with jiti moduleCache:false, so this module
|
||||
// is re-executed on every /reload. Re-importing the fff-bun module graph
|
||||
// (which top-level awaits a `type: "file"` import of the native .so) hangs
|
||||
// forever inside the Bun-compiled pi binary. Cache the first import on
|
||||
// globalThis so reloads reuse the resolved module instead of re-importing.
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
if (g.__fffSdkPromiseGlobal) {
|
||||
sdkPromise = g.__fffSdkPromiseGlobal as Promise<{ FileFinder: FileFinderStatic }>;
|
||||
return sdkPromise;
|
||||
}
|
||||
|
||||
// default to node as it seems like default option
|
||||
const pkg = detectRuntime() === "bun" ? "@ff-labs/fff-bun" : "@ff-labs/fff-node";
|
||||
sdkPromise = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>;
|
||||
return sdkPromise;
|
||||
const p = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>;
|
||||
sdkPromise = p;
|
||||
(globalThis as Record<string, unknown>).__fffSdkPromiseGlobal = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -36,11 +36,22 @@ mock.module("@ff-labs/fff-node", () => finderModule);
|
||||
mock.module("@ff-labs/fff-bun", () => finderModule);
|
||||
|
||||
const { AuxFinderPool } = await import("../src/aux-finders");
|
||||
const { FilePickerFactory } = await import("../src/file-picker");
|
||||
|
||||
function makePickers() {
|
||||
return new FilePickerFactory({
|
||||
frecencyDbPath: "/dbs/frecency",
|
||||
historyDbPath: "/dbs/history",
|
||||
});
|
||||
}
|
||||
|
||||
describe("AuxFinderPool concurrent dedup (#746)", () => {
|
||||
test("two concurrent acquires for same root share one finder", async () => {
|
||||
created.length = 0;
|
||||
const pool = new AuxFinderPool({ enableFsRootScanning: false });
|
||||
const pool = new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
});
|
||||
const [a, b] = await Promise.all([
|
||||
pool.acquire("/Users/x"),
|
||||
pool.acquire("/Users/x"),
|
||||
@@ -51,7 +62,10 @@ describe("AuxFinderPool concurrent dedup (#746)", () => {
|
||||
|
||||
test("sequential acquire after in-flight one resolves still reuses", async () => {
|
||||
created.length = 0;
|
||||
const pool = new AuxFinderPool({ enableFsRootScanning: false });
|
||||
const pool = new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
});
|
||||
const first = pool.acquire("/Users/x");
|
||||
const second = pool.acquire("/Users/x");
|
||||
await Promise.all([first, second]);
|
||||
|
||||
@@ -25,10 +25,19 @@ function createMockFinder(basePath: string): MockFinder {
|
||||
return finder;
|
||||
}
|
||||
|
||||
// Set to make db-backed creates fail, mimicking a corrupt/locked LMDB.
|
||||
let failDbCreates = false;
|
||||
// Set to make every create fail, db-backed or not.
|
||||
let failAllCreates = false;
|
||||
|
||||
const finderModule = {
|
||||
FileFinder: {
|
||||
create: (options: Record<string, unknown>) => {
|
||||
createOptions.push(options);
|
||||
if (failAllCreates || (failDbCreates && options.frecencyDbPath !== undefined)) {
|
||||
return { ok: false as const, error: "db locked" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: createMockFinder(options.basePath as string),
|
||||
@@ -41,11 +50,26 @@ mock.module("@ff-labs/fff-node", () => finderModule);
|
||||
mock.module("@ff-labs/fff-bun", () => finderModule);
|
||||
|
||||
const { AuxFinderPool } = await import("../src/aux-finders");
|
||||
const { FilePickerFactory } = await import("../src/file-picker");
|
||||
|
||||
function makePool(opts: Record<string, unknown> = {}) {
|
||||
created.length = 0;
|
||||
createOptions.length = 0;
|
||||
return new AuxFinderPool({ enableFsRootScanning: false, ...opts });
|
||||
failDbCreates = false;
|
||||
failAllCreates = false;
|
||||
return new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function makePickers(onDbFailure?: (error: string) => void) {
|
||||
return new FilePickerFactory({
|
||||
frecencyDbPath: "/dbs/frecency",
|
||||
historyDbPath: "/dbs/history",
|
||||
onDbFailure,
|
||||
});
|
||||
}
|
||||
|
||||
describe("AuxFinderPool covering reuse", () => {
|
||||
@@ -97,7 +121,7 @@ describe("AuxFinderPool covering reuse", () => {
|
||||
// Regression for #743: the agent spawning an aux picker over $HOME must warn
|
||||
// the user every time, not silently walk the home tree.
|
||||
test("notifies on every aux picker that covers $HOME", async () => {
|
||||
const onHomeDirScan = mock(() => undefined);
|
||||
const onHomeDirScan = mock((_root: string) => undefined);
|
||||
const pool = makePool({ onHomeDirScan });
|
||||
const home = os.homedir();
|
||||
|
||||
@@ -119,16 +143,63 @@ describe("AuxFinderPool covering reuse", () => {
|
||||
expect(createOptions[0].enableHomeDirScanning).toBe(false);
|
||||
});
|
||||
|
||||
// Regression for #700: aux finders must not reopen the main frecency/history
|
||||
// LMDB envs, or heed fails with "environment already open in this program".
|
||||
test("aux finders are created without frecency/history db paths", async () => {
|
||||
// #700 is fixed by the process-wide LMDB env pool: same-path opens share one
|
||||
// env, so aux finders now reuse the session's frecency/history DBs.
|
||||
test("aux finders receive the pool's frecency/history db paths", async () => {
|
||||
const pool = makePool();
|
||||
await pool.acquire("/a/b/c");
|
||||
await pool.acquire("/x/y");
|
||||
expect(createOptions.length).toBe(2);
|
||||
for (const opts of createOptions) {
|
||||
expect(opts.frecencyDbPath).toBeUndefined();
|
||||
expect(opts.historyDbPath).toBeUndefined();
|
||||
expect(opts.frecencyDbPath).toBe("/dbs/frecency");
|
||||
expect(opts.historyDbPath).toBe("/dbs/history");
|
||||
}
|
||||
});
|
||||
|
||||
test("aux finder falls back to no dbs when opening them fails", async () => {
|
||||
const failures: string[] = [];
|
||||
const pool = makePool({ pickers: makePickers((e) => failures.push(e)) });
|
||||
failDbCreates = true;
|
||||
|
||||
const entry = await pool.acquire("/a/b/c");
|
||||
|
||||
expect(entry.root).toBe("/a/b/c");
|
||||
expect(createOptions.length).toBe(2);
|
||||
expect(createOptions[0].frecencyDbPath).toBe("/dbs/frecency");
|
||||
expect(createOptions[1].frecencyDbPath).toBeUndefined();
|
||||
expect(failures).toEqual(["db locked"]);
|
||||
});
|
||||
|
||||
test("a db failure on the main finder keeps later aux finders db-less", async () => {
|
||||
const failures: string[] = [];
|
||||
const pickers = makePickers((e) => failures.push(e));
|
||||
const pool = makePool({ pickers });
|
||||
failDbCreates = true;
|
||||
|
||||
// Stands in for the main cwd picker hitting the broken db first.
|
||||
// The SDK is mocked, so create() hands back a MockFinder, not a real finder.
|
||||
const main = (await pickers.create({
|
||||
basePath: "/workspace",
|
||||
})) as unknown as MockFinder;
|
||||
expect(main.basePath).toBe("/workspace");
|
||||
expect(pickers.databasesDisabled).toBe(true);
|
||||
|
||||
createOptions.length = 0;
|
||||
await pool.acquire("/a/b/c");
|
||||
|
||||
// No retry: the factory already gave up on the dbs, so one db-less create.
|
||||
expect(createOptions.length).toBe(1);
|
||||
expect(createOptions[0].frecencyDbPath).toBeUndefined();
|
||||
expect(createOptions[0].historyDbPath).toBeUndefined();
|
||||
expect(failures).toEqual(["db locked"]);
|
||||
});
|
||||
|
||||
test("create throws when the picker cannot be opened at all", async () => {
|
||||
makePool();
|
||||
failAllCreates = true;
|
||||
|
||||
expect(makePickers().create({ basePath: "/nope" })).rejects.toThrow(
|
||||
"Failed to create FFF file picker for /nope: db locked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { CONFIG_FILE_NAME, loadConfig } from "../src/config";
|
||||
|
||||
describe("loadConfig", () => {
|
||||
let agentDir: string;
|
||||
let configPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fff-config-"));
|
||||
configPath = path.join(agentDir, CONFIG_FILE_NAME);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("returns an empty config when the file does not exist", () => {
|
||||
expect(loadConfig(agentDir)).toEqual({});
|
||||
});
|
||||
|
||||
test("loads every supported option", () => {
|
||||
const config = {
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/dmtrKovalenko/fff/main/packages/pi-fff/pi-fff.schema.json",
|
||||
mode: "override" as const,
|
||||
frecencyDbPath: "/data/frecency",
|
||||
historyDbPath: "/data/history",
|
||||
enableFsRootScanning: true,
|
||||
enableHomeDirScanning: false,
|
||||
};
|
||||
writeConfig(config);
|
||||
|
||||
expect(loadConfig(agentDir)).toEqual(config);
|
||||
});
|
||||
|
||||
test("rejects malformed JSON", () => {
|
||||
fs.writeFileSync(configPath, '{"mode":');
|
||||
|
||||
expect(() => loadConfig(agentDir)).toThrow(
|
||||
`Invalid pi-fff config at ${configPath}: not valid JSON`,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects non-object config", () => {
|
||||
writeConfig(["override"]);
|
||||
|
||||
expect(() => loadConfig(agentDir)).toThrow("expected a JSON object");
|
||||
});
|
||||
|
||||
test("rejects unknown options", () => {
|
||||
writeConfig({ mode: "override", typo: true });
|
||||
|
||||
expect(() => loadConfig(agentDir)).toThrow('unknown option "typo"');
|
||||
});
|
||||
|
||||
test("rejects invalid option values", () => {
|
||||
const cases: [Record<string, unknown>, string][] = [
|
||||
[{ $schema: false }, '"$schema" must be a non-empty string'],
|
||||
[{ $schema: "" }, '"$schema" must be a non-empty string'],
|
||||
[{ mode: "replace" }, '"mode" must be one of'],
|
||||
[{ frecencyDbPath: "" }, '"frecencyDbPath" must be a non-empty string'],
|
||||
[{ historyDbPath: false }, '"historyDbPath" must be a non-empty string'],
|
||||
[{ enableFsRootScanning: 1 }, '"enableFsRootScanning" must be a boolean'],
|
||||
[{ enableHomeDirScanning: "false" }, '"enableHomeDirScanning" must be a boolean'],
|
||||
];
|
||||
|
||||
for (const [config, message] of cases) {
|
||||
writeConfig(config);
|
||||
expect(() => loadConfig(agentDir)).toThrow(message);
|
||||
}
|
||||
});
|
||||
|
||||
test("reports file read failures", () => {
|
||||
fs.mkdirSync(configPath);
|
||||
|
||||
expect(() => loadConfig(agentDir)).toThrow(
|
||||
`Could not read pi-fff config at ${configPath}`,
|
||||
);
|
||||
});
|
||||
|
||||
function writeConfig(config: unknown): void {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveDbPaths } from "../src/paths";
|
||||
|
||||
const ENV_KEYS = ["XDG_CACHE_HOME", "XDG_DATA_HOME", "PI_CODING_AGENT_DIR"] as const;
|
||||
|
||||
describe("resolveDbPaths", () => {
|
||||
let tmpRoot: string;
|
||||
let piDir: string;
|
||||
let saved: Record<string, string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
saved = {};
|
||||
for (const key of ENV_KEYS) saved[key] = process.env[key];
|
||||
|
||||
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "fff-db-paths-"));
|
||||
piDir = path.join(tmpRoot, "pi-agent");
|
||||
|
||||
process.env.XDG_CACHE_HOME = path.join(tmpRoot, "cache");
|
||||
process.env.XDG_DATA_HOME = path.join(tmpRoot, "data");
|
||||
process.env.PI_CODING_AGENT_DIR = piDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
const value = saved[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("overrides win over discovery and fallback", () => {
|
||||
mkNvimDir("cache", "fff_nvim");
|
||||
mkNvimDir("data", "fff_queries");
|
||||
|
||||
const paths = resolveDbPaths({
|
||||
frecency: "/explicit/frecency",
|
||||
history: "/explicit/history",
|
||||
});
|
||||
|
||||
expect(paths.frecency).toBe("/explicit/frecency");
|
||||
expect(paths.history).toBe("/explicit/history");
|
||||
});
|
||||
|
||||
test("picks existing fff.nvim databases", () => {
|
||||
const frecency = mkNvimDir("cache", "fff_nvim");
|
||||
const history = mkNvimDir("data", "fff_queries");
|
||||
|
||||
expect(resolveDbPaths({})).toEqual({ frecency, history });
|
||||
});
|
||||
|
||||
test("uses the pi data dir when no nvim databases exist", () => {
|
||||
expect(resolveDbPaths({})).toEqual({
|
||||
frecency: path.join(piDir, "fff", "frecency"),
|
||||
history: path.join(piDir, "fff", "history"),
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores a plain file at the nvim candidate path", () => {
|
||||
const cacheDir = path.join(tmpRoot, "cache", "nvim");
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(cacheDir, "fff_nvim"), "not a db");
|
||||
|
||||
expect(resolveDbPaths({}).frecency).toBe(path.join(piDir, "fff", "frecency"));
|
||||
});
|
||||
|
||||
test("resolves each database independently", () => {
|
||||
const history = mkNvimDir("data", "fff_queries");
|
||||
const paths = resolveDbPaths({ frecency: "/explicit/frecency" });
|
||||
|
||||
expect(paths.frecency).toBe("/explicit/frecency");
|
||||
expect(paths.history).toBe(history);
|
||||
});
|
||||
|
||||
function mkNvimDir(kind: "cache" | "data", name: string): string {
|
||||
const dir = path.join(tmpRoot, kind, "nvim", name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
type MockFinder = {
|
||||
isDestroyed: boolean;
|
||||
@@ -87,7 +89,7 @@ mock.module("@sinclair/typebox", () => ({
|
||||
properties,
|
||||
options,
|
||||
}),
|
||||
Optional: (value: unknown) => ({ ...value, optional: true }),
|
||||
Optional: (value: Record<string, unknown>) => ({ ...value, optional: true }),
|
||||
String: schema("string"),
|
||||
Union: (items: unknown[], options?: unknown) => ({ type: "union", items, options }),
|
||||
},
|
||||
@@ -97,20 +99,38 @@ const { default: fffExtension } = await import("../src/index");
|
||||
|
||||
type EventHandler = (...args: any[]) => unknown;
|
||||
|
||||
function createPi(mode?: string) {
|
||||
function createPi(mode?: string, flags: Record<string, unknown> = {}) {
|
||||
const events = new Map<string, EventHandler>();
|
||||
const commands = new Map<string, any>();
|
||||
const registeredFlags = new Set<string>();
|
||||
let flagsReady = false;
|
||||
|
||||
const pi = {
|
||||
getFlag: mock((name: string) => (name === "fff-mode" ? mode : undefined)),
|
||||
getFlag: mock((name: string) => {
|
||||
if (!flagsReady || !registeredFlags.has(name)) return undefined;
|
||||
return name === "fff-mode" && mode !== undefined ? mode : flags[name];
|
||||
}),
|
||||
on: mock((event: string, handler: EventHandler) => {
|
||||
events.set(event, handler);
|
||||
events.set(event, (...args) => {
|
||||
flagsReady = true;
|
||||
return handler(...args);
|
||||
});
|
||||
}),
|
||||
registerCommand: mock((name: string, command: any) => {
|
||||
commands.set(name, command);
|
||||
commands.set(name, {
|
||||
...command,
|
||||
handler: (...args: any[]) => {
|
||||
flagsReady = true;
|
||||
return command.handler(...args);
|
||||
},
|
||||
});
|
||||
}),
|
||||
registerFlag: mock(() => undefined),
|
||||
registerTool: mock(() => undefined),
|
||||
registerFlag: mock((name: string) => {
|
||||
registeredFlags.add(name);
|
||||
}),
|
||||
registerTool: mock((_tool: any) => undefined),
|
||||
getActiveTools: mock(() => ["read"] as string[]),
|
||||
setActiveTools: mock((_names: string[]) => undefined),
|
||||
appendEntry: mock(() => undefined),
|
||||
};
|
||||
|
||||
@@ -120,17 +140,21 @@ function createPi(mode?: string) {
|
||||
function createContext(cwd = "/tmp/workspace") {
|
||||
return {
|
||||
cwd,
|
||||
sessionManager: {
|
||||
getEntries: mock(() => [] as any[]),
|
||||
},
|
||||
// Signatures mirror the real pi UI surface so mock.calls stays typed.
|
||||
ui: {
|
||||
addAutocompleteProvider: mock(() => undefined),
|
||||
notify: mock(() => undefined),
|
||||
addAutocompleteProvider: mock((_factory: (current: any) => any) => undefined),
|
||||
notify: mock((_message: string, _level?: string) => undefined),
|
||||
setEditorComponent: mock(() => undefined),
|
||||
setStatus: mock(() => undefined),
|
||||
setStatus: mock((_key: string, _text?: string) => undefined),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function start(mode?: string, cwd?: string) {
|
||||
const setup = createPi(mode);
|
||||
async function start(mode?: string, cwd?: string, flags: Record<string, unknown> = {}) {
|
||||
const setup = createPi(mode, flags);
|
||||
const ctx = createContext(cwd);
|
||||
fffExtension(setup.pi as any);
|
||||
|
||||
@@ -159,13 +183,186 @@ function abortOptions() {
|
||||
return { signal: new AbortController().signal };
|
||||
}
|
||||
|
||||
const CONFIG_ENV_KEYS = [
|
||||
"PI_CODING_AGENT_DIR",
|
||||
"PI_FFF_MODE",
|
||||
"FFF_FRECENCY_DB",
|
||||
"FFF_HISTORY_DB",
|
||||
"FFF_ENABLE_ROOT_SCAN",
|
||||
"FFF_ENABLE_HOME_SCAN",
|
||||
] as const;
|
||||
|
||||
const savedEnv: Record<string, string | undefined> = {};
|
||||
for (const key of CONFIG_ENV_KEYS) savedEnv[key] = process.env[key];
|
||||
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fff-extension-"));
|
||||
const configPath = path.join(agentDir, "pi-fff.json");
|
||||
|
||||
beforeEach(() => {
|
||||
createCalls.length = 0;
|
||||
finders = [];
|
||||
mixedSearchImpl = undefined;
|
||||
scanProgressImpl = undefined;
|
||||
delete process.env.PI_FFF_MODE;
|
||||
delete process.env.FFF_ENABLE_HOME_SCAN;
|
||||
|
||||
for (const key of CONFIG_ENV_KEYS) delete process.env[key];
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
fs.rmSync(configPath, { force: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
for (const key of CONFIG_ENV_KEYS) {
|
||||
const value = savedEnv[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("pi-fff global config", () => {
|
||||
test("applies every supported startup option", async () => {
|
||||
writeConfig({
|
||||
mode: "override",
|
||||
frecencyDbPath: "/config/frecency",
|
||||
historyDbPath: "/config/history",
|
||||
enableFsRootScanning: true,
|
||||
enableHomeDirScanning: false,
|
||||
});
|
||||
|
||||
const setup = await start();
|
||||
const toolNames = setup.pi.registerTool.mock.calls.map(([tool]) => tool.name);
|
||||
|
||||
expect(toolNames).toContain("grep");
|
||||
expect(toolNames).toContain("find");
|
||||
expect(toolNames).not.toContain("ffgrep");
|
||||
expect(createCalls[0]).toEqual({
|
||||
basePath: "/tmp/workspace",
|
||||
frecencyDbPath: "/config/frecency",
|
||||
historyDbPath: "/config/history",
|
||||
aiMode: true,
|
||||
enableHomeDirScanning: false,
|
||||
enableFsRootScanning: true,
|
||||
});
|
||||
await shutdown(setup);
|
||||
});
|
||||
|
||||
test("keeps flag and environment precedence", async () => {
|
||||
writeConfig({
|
||||
mode: "tools-only",
|
||||
frecencyDbPath: "/config/frecency",
|
||||
historyDbPath: "/config/history",
|
||||
enableFsRootScanning: true,
|
||||
enableHomeDirScanning: false,
|
||||
});
|
||||
process.env.PI_FFF_MODE = "override";
|
||||
process.env.FFF_FRECENCY_DB = "/env/frecency";
|
||||
process.env.FFF_HISTORY_DB = "/env/history";
|
||||
process.env.FFF_ENABLE_ROOT_SCAN = "1";
|
||||
process.env.FFF_ENABLE_HOME_SCAN = "1";
|
||||
|
||||
const setup = await start("tools-and-ui", undefined, {
|
||||
"fff-frecency-db": "/flag/frecency",
|
||||
"fff-enable-root-scan": false,
|
||||
});
|
||||
const toolNames = setup.pi.registerTool.mock.calls.map(([tool]) => tool.name);
|
||||
|
||||
expect(toolNames).toContain("ffgrep");
|
||||
expect(toolNames).toContain("fffind");
|
||||
expect(createCalls[0]).toEqual({
|
||||
basePath: "/tmp/workspace",
|
||||
frecencyDbPath: "/flag/frecency",
|
||||
historyDbPath: "/env/history",
|
||||
aiMode: true,
|
||||
enableHomeDirScanning: true,
|
||||
enableFsRootScanning: false,
|
||||
});
|
||||
await shutdown(setup);
|
||||
});
|
||||
|
||||
test("falls through invalid flag and environment modes", async () => {
|
||||
writeConfig({ mode: "override" });
|
||||
process.env.PI_FFF_MODE = "invalid-env-mode";
|
||||
|
||||
const setup = await start("invalid-flag-mode");
|
||||
const toolNames = setup.pi.registerTool.mock.calls.map(([tool]) => tool.name);
|
||||
|
||||
expect(toolNames).toContain("grep");
|
||||
expect(toolNames).toContain("find");
|
||||
expect(toolNames).not.toContain("ffgrep");
|
||||
await shutdown(setup);
|
||||
});
|
||||
});
|
||||
|
||||
function writeConfig(config: Record<string, unknown>): void {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config));
|
||||
}
|
||||
|
||||
describe("pi-fff session mode", () => {
|
||||
test("registers tools only after restoring the saved mode", async () => {
|
||||
const setup = createPi("tools-and-ui");
|
||||
const ctx = createContext();
|
||||
ctx.sessionManager.getEntries.mockReturnValue([
|
||||
{ type: "custom", customType: "fff-mode", data: { mode: "override" } },
|
||||
]);
|
||||
fffExtension(setup.pi as any);
|
||||
|
||||
expect(setup.pi.registerTool).not.toHaveBeenCalled();
|
||||
await setup.events.get("session_start")?.({ reason: "startup" }, ctx);
|
||||
|
||||
const tools = setup.pi.registerTool.mock.calls.map(([tool]) => tool);
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("grep");
|
||||
expect(toolNames).toContain("find");
|
||||
expect(toolNames).not.toContain("ffgrep");
|
||||
expect(toolNames).not.toContain("fffind");
|
||||
const grepTool = tools.find((tool) => tool.name === "grep");
|
||||
expect(grepTool.promptGuidelines[0].startsWith("grep:")).toBe(true);
|
||||
expect(setup.pi.setActiveTools).toHaveBeenCalledWith(
|
||||
expect.arrayContaining(["read", "grep", "find"]),
|
||||
);
|
||||
|
||||
await setup.commands.get("fff-mode").handler("", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Current mode: 'override' (flag: tools-and-ui)",
|
||||
"info",
|
||||
);
|
||||
await shutdown(setup);
|
||||
});
|
||||
|
||||
test("registers tools before an unbound SDK session's first agent turn", async () => {
|
||||
const setup = createPi("override");
|
||||
const ctx = createContext();
|
||||
fffExtension(setup.pi as any);
|
||||
|
||||
expect(setup.pi.registerTool).not.toHaveBeenCalled();
|
||||
await setup.events.get("before_agent_start")?.({}, ctx);
|
||||
|
||||
const toolNames = setup.pi.registerTool.mock.calls.map(([tool]) => tool.name);
|
||||
expect(toolNames).toContain("grep");
|
||||
expect(toolNames).toContain("find");
|
||||
expect(createCalls).toHaveLength(0);
|
||||
await shutdown(setup);
|
||||
});
|
||||
|
||||
test("keeps the active mode unchanged until a tool-name switch is reloaded", async () => {
|
||||
const setup = await start();
|
||||
|
||||
await setup.commands.get("fff-mode").handler("override", setup.ctx);
|
||||
|
||||
expect(setup.pi.appendEntry).toHaveBeenCalledWith("fff-mode", {
|
||||
mode: "override",
|
||||
});
|
||||
expect(setup.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Mode 'override' saved. Run /reload to apply the tool name change.",
|
||||
"info",
|
||||
);
|
||||
|
||||
await setup.commands.get("fff-mode").handler("", setup.ctx);
|
||||
expect(setup.ctx.ui.notify).toHaveBeenLastCalledWith(
|
||||
"Current mode: 'tools-and-ui' (flag: unset)",
|
||||
"info",
|
||||
);
|
||||
await shutdown(setup);
|
||||
});
|
||||
});
|
||||
|
||||
// Regression for #743: launching from $HOME must be visible and interruptible.
|
||||
@@ -211,9 +408,9 @@ describe("pi-fff $HOME scan warning", () => {
|
||||
});
|
||||
const setup = await start(undefined, os.homedir());
|
||||
|
||||
const [key, text] = setup.ctx.ui.setStatus.mock.calls.at(-1) as [string, string];
|
||||
expect(key).toBe("fff");
|
||||
expect(text).toContain("12345 files");
|
||||
const lastStatus = setup.ctx.ui.setStatus.mock.calls.at(-1);
|
||||
expect(lastStatus?.[0]).toBe("fff");
|
||||
expect(lastStatus?.[1]).toContain("12345 files");
|
||||
|
||||
// session_shutdown must stop the poller and clear the footer.
|
||||
await shutdown(setup);
|
||||
@@ -239,8 +436,9 @@ describe("pi-fff autocomplete registration", () => {
|
||||
expect(createCalls).toEqual([
|
||||
{
|
||||
basePath: "/tmp/workspace",
|
||||
frecencyDbPath: undefined,
|
||||
historyDbPath: undefined,
|
||||
// Resolved defaults are host-dependent; covered by test/db-paths.test.ts.
|
||||
frecencyDbPath: expect.any(String),
|
||||
historyDbPath: expect.any(String),
|
||||
aiMode: true,
|
||||
enableHomeDirScanning: true,
|
||||
enableFsRootScanning: false,
|
||||
@@ -380,7 +578,7 @@ describe("pi-fff autocomplete registration", () => {
|
||||
});
|
||||
|
||||
test("/fff-mode changes mention behavior without touching the editor", async () => {
|
||||
const { commands, ctx } = await start();
|
||||
const { commands, ctx, pi } = await start();
|
||||
const factory = ctx.ui.addAutocompleteProvider.mock.calls[0][0];
|
||||
const current = currentProvider();
|
||||
const provider = factory(current);
|
||||
@@ -388,6 +586,7 @@ describe("pi-fff autocomplete registration", () => {
|
||||
await commands.get("fff-mode").handler("tools-only", ctx);
|
||||
await provider.getSuggestions(["@src"], 0, 4, abortOptions());
|
||||
|
||||
expect(pi.appendEntry).toHaveBeenCalledWith("fff-mode", { mode: "tools-only" });
|
||||
expect(current.getSuggestions).toHaveBeenCalledTimes(1);
|
||||
expect(finders[0].mixedSearch).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.setEditorComponent).not.toHaveBeenCalled();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,4 +77,4 @@ git push origin "$TAG"
|
||||
|
||||
echo ""
|
||||
echo "Release $VERSION created and pushed."
|
||||
echo "CI will build and publish: https://github.com/dmtrKovalenko/fff.nvim/actions"
|
||||
echo "CI will build and publish: https://github.com/dmtrKovalenko/fff/actions"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user