@ff-labs/fff-bun 0.8.5-nightly.ccb1b9d → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,257 +1,102 @@
1
1
  # fff - Fast File Finder
2
2
 
3
- High-performance fuzzy file finder for Bun, powered by Rust. Perfect for LLM agent tools that need to search through codebases.
3
+ High-performance fuzzy file finder for Bun, powered by Rust. Extremely fast live file, content, and directory search with a typo-resistant algorithm. As well as regex, plain-text, multi-occurrence and typo-resistant content search.
4
4
 
5
- ## Features
5
+ Comes with built-in git status support, frecency access tracking, and a real-time file watcher, content indexing and many more! Designed for LLM agent tools that search through codebases or agentic RAG document search.
6
6
 
7
- - **Blazing fast** - Rust-powered fuzzy search with parallel processing
8
- - **Smart ranking** - Frecency-based scoring (frequency + recency)
9
- - **Git-aware** - Shows file git status in results
10
- - **Query history** - Learns from your search patterns
11
- - **Type-safe** - Full TypeScript support with Result types
7
+ Faster than ripgrep & fzf on any workflow that runs more than once per process.
12
8
 
13
9
  ## Installation
14
10
 
15
11
  ```bash
16
- bun add @ff-labs/bun
12
+ bun add @ff-labs/fff-bun
17
13
  ```
18
14
 
19
- The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bin-darwin-arm64`, `@ff-labs/fff-bin-linux-x64-gnu`). No GitHub downloads are needed.
15
+ The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bin-darwin-arm64`, `@ff-labs/fff-bin-linux-x64-gnu`)
20
16
 
21
17
  ### Supported Platforms
22
18
 
23
- | Platform | Architecture | Package |
24
- |----------|-------------|---------|
25
- | macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bin-darwin-arm64` |
26
- | macOS | x64 (Intel) | `@ff-labs/fff-bin-darwin-x64` |
27
- | Linux | x64 (glibc) | `@ff-labs/fff-bin-linux-x64-gnu` |
28
- | Linux | ARM64 (glibc) | `@ff-labs/fff-bin-linux-arm64-gnu` |
29
- | Linux | x64 (musl) | `@ff-labs/fff-bin-linux-x64-musl` |
30
- | Linux | ARM64 (musl) | `@ff-labs/fff-bin-linux-arm64-musl` |
31
- | Windows | x64 | `@ff-labs/fff-bin-win32-x64` |
32
- | Windows | ARM64 | `@ff-labs/fff-bin-win32-arm64` |
19
+ | Platform | Architecture | Package |
20
+ | -------- | --------------------- | ----------------------------------- |
21
+ | macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bin-darwin-arm64` |
22
+ | macOS | x64 (Intel) | `@ff-labs/fff-bin-darwin-x64` |
23
+ | Linux | x64 (glibc) | `@ff-labs/fff-bin-linux-x64-gnu` |
24
+ | Linux | ARM64 (glibc) | `@ff-labs/fff-bin-linux-arm64-gnu` |
25
+ | Linux | x64 (musl) | `@ff-labs/fff-bin-linux-x64-musl` |
26
+ | Linux | ARM64 (musl) | `@ff-labs/fff-bin-linux-arm64-musl` |
27
+ | Windows | x64 | `@ff-labs/fff-bin-win32-x64` |
28
+ | Windows | ARM64 | `@ff-labs/fff-bin-win32-arm64` |
33
29
 
34
30
  If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback.
35
31
 
36
32
  ## Quick Start
37
33
 
38
- ```typescript
39
- import { FileFinder } from "fff";
40
-
41
- // Initialize with a directory
42
- const result = FileFinder.init({ basePath: "/path/to/project" });
43
- if (!result.ok) {
44
- console.error(result.error);
45
- process.exit(1);
46
- }
47
-
48
- // Wait for initial scan
49
- FileFinder.waitForScan(5000);
50
-
51
- // Search for files
52
- const search = FileFinder.search("main.ts");
53
- if (search.ok) {
54
- for (const item of search.value.items) {
55
- console.log(item.relativePath);
56
- }
57
- }
58
-
59
- // Cleanup when done
60
- FileFinder.destroy();
61
- ```
62
-
63
- ## API Reference
64
-
65
- ### `FileFinder.init(options)`
66
-
67
- Initialize the file finder.
68
-
69
- ```typescript
70
- interface InitOptions {
71
- basePath: string; // Directory to index (required)
72
- frecencyDbPath?: string; // Frecency DB path (omit to skip frecency)
73
- historyDbPath?: string; // History DB path (omit to skip query tracking)
74
- useUnsafeNoLock?: boolean; // Faster but less safe DB mode
75
- }
76
-
77
- const result = FileFinder.init({ basePath: "/my/project" });
78
- ```
79
-
80
- ### `FileFinder.search(query, options?)`
81
-
82
- Search for files.
83
-
84
- ```typescript
85
- interface SearchOptions {
86
- maxThreads?: number; // Parallel threads (0 = auto)
87
- currentFile?: string; // Deprioritize this file
88
- comboBoostMultiplier?: number; // Query history boost
89
- minComboCount?: number; // Min history matches
90
- pageIndex?: number; // Pagination offset
91
- pageSize?: number; // Results per page
92
- }
93
-
94
- const result = FileFinder.search("main.ts", { pageSize: 10 });
95
- if (result.ok) {
96
- console.log(`Found ${result.value.totalMatched} files`);
97
- }
98
- ```
99
-
100
- ### Query Syntax
101
-
102
- - `foo bar` - Match files containing "foo" and "bar"
103
- - `src/` - Match files in src directory
104
- - `file.ts:42` - Match file.ts with line 42
105
- - `file.ts:42:10` - Match with line and column
106
-
107
- ### `FileFinder.trackAccess(filePath)`
108
-
109
- Track file access for frecency scoring.
34
+ Each `FileFinder` instance owns an independent native index. Create one, wait
35
+ for the initial scan, then run as many searches as you like.
110
36
 
111
37
  ```typescript
112
- // Call when user opens a file
113
- FileFinder.trackAccess("/path/to/file.ts");
114
- ```
38
+ import { FileFinder } from "@ff-labs/fff-bun";
115
39
 
116
- ### `FileFinder.grep(query, options?)`
40
+ // Create an instance bound to a directory
41
+ const created = FileFinder.create({ basePath: "/path/to/project" });
42
+ if (!created.ok) throw new Error(created.error);
117
43
 
118
- Search file contents with SIMD-accelerated matching.
44
+ const finder = created.value;
119
45
 
120
- ```typescript
121
- interface GrepOptions {
122
- maxFileSize?: number; // Max file size in bytes (default: 10MB)
123
- maxMatchesPerFile?: number; // Max matches per file (default: 200, set 0 to unlimited)
124
- smartCase?: boolean; // Case-insensitive if all lowercase (default: true)
125
- fileOffset?: number; // Pagination offset (default: 0)
126
- pageLimit?: number; // Max matches to return (default: 50)
127
- mode?: "plain" | "regex" | "fuzzy"; // Search mode (default: "plain")
128
- timeBudgetMs?: number; // Time limit in ms, 0 = unlimited (default: 0)
129
- }
46
+ // Wait for the initial scan (non-blocking)
47
+ await finder.waitForScan(5000);
130
48
 
131
- // Plain text search
132
- const result = FileFinder.grep("TODO", { pageLimit: 20 });
133
- if (result.ok) {
134
- for (const match of result.value.items) {
135
- console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
49
+ // 1. Fuzzy file search (typo resistant)
50
+ const files = finder.fileSearch("typescropt.ts", { pageSize: 10 });
51
+ if (files.ok) {
52
+ for (const item of files.value.items) {
53
+ console.log(item.relativePath, item.gitStatus);
136
54
  }
137
55
  }
138
56
 
139
- // Regex search
140
- const regexResult = FileFinder.grep("fn\\s+\\w+", { mode: "regex" });
57
+ // 2. Glob filter — no fuzzy matching, 100% compatible with npm `glob`
58
+ const globbed = finder.glob("src/**/*.ts");
59
+ if (globbed.ok) console.log(`${globbed.value.totalMatched} TypeScript files`);
141
60
 
142
- // Fuzzy search
143
- const fuzzyResult = FileFinder.grep("imprt recat", { mode: "fuzzy" });
144
-
145
- // Pagination
146
- const page1 = FileFinder.grep("error");
147
- if (page1.ok && page1.value.nextCursor) {
148
- const page2 = FileFinder.grep("error", {
149
- cursor: page1.value.nextCursor,
150
- });
61
+ // 3. Content search (live grep) with pagination
62
+ const grep = finder.grep("TODO", { mode: "plain", pageSize: 20 });
63
+ if (grep.ok) {
64
+ for (const m of grep.value.items) {
65
+ console.log(`${m.relativePath}:${m.lineNumber}: ${m.lineContent}`);
66
+ }
151
67
  }
152
68
 
153
- // With file constraints
154
- const tsOnly = FileFinder.grep("*.ts useState");
155
- const srcOnly = FileFinder.grep("src/ handleClick");
156
- ```
157
-
158
- ### `FileFinder.trackQuery(query, selectedFile)`
69
+ // 4. Directory search based on the query (typo resistant)
70
+ const dirs = finder.directorySearch("components");
71
+ if (dirs.ok) console.log(dirs.value.items.map((d) => d.relativePath));
159
72
 
160
- Track query completion for smart suggestions.
161
-
162
- ```typescript
163
- // Call when user selects a file from search
164
- FileFinder.trackQuery("main", "/path/to/main.ts");
73
+ // Free the resources when you don't need a file picker anymore
74
+ finder.destroy();
165
75
  ```
166
76
 
167
- ### `FileFinder.healthCheck(testPath?)`
168
-
169
- Get diagnostic information.
170
-
171
- ```typescript
172
- const health = FileFinder.healthCheck();
173
- if (health.ok) {
174
- console.log(`Version: ${health.value.version}`);
175
- console.log(`Indexed: ${health.value.filePicker.indexedFiles} files`);
176
- }
177
- ```
178
77
 
179
- ### Other Methods
78
+ ## API Reference
180
79
 
181
- - `FileFinder.grep(query, options?)` - Search file contents
182
- - `FileFinder.scanFiles()` - Trigger rescan
183
- - `FileFinder.isScanning()` - Check scan status
184
- - `FileFinder.getScanProgress()` - Get scan progress
185
- - `FileFinder.waitForScan(timeoutMs)` - Wait for scan
186
- - `FileFinder.reindex(newPath)` - Change indexed directory
187
- - `FileFinder.refreshGitStatus()` - Refresh git cache
188
- - `FileFinder.getHistoricalQuery(offset)` - Get past queries
189
- - `FileFinder.destroy()` - Cleanup resources
80
+ Verify the latest API in the local interface at [`./src/fff-api.ts`](./src/fff-api.ts). Every field and type is documented.
190
81
 
191
- ## Result Types
82
+ ### Result Types
192
83
 
193
84
  All methods return a `Result<T>` type for explicit error handling:
194
85
 
86
+
195
87
  ```typescript
196
- type Result<T> =
197
- | { ok: true; value: T }
198
- | { ok: false; error: string };
88
+ type Result<T> = { ok: true; value: T } | { ok: false; error: string };
89
+
90
+ const result = finder.fileSearch("foo");
199
91
 
200
- const result = FileFinder.search("foo");
201
92
  if (result.ok) {
202
93
  // result.value is SearchResult
203
94
  } else {
204
- // result.error is string
95
+ // result.error is string error message
205
96
  }
206
97
  ```
207
98
 
208
- ## Search Result Types
209
-
210
- ```typescript
211
- interface SearchResult {
212
- items: FileItem[];
213
- scores: Score[];
214
- totalMatched: number;
215
- totalFiles: number;
216
- location?: Location;
217
- }
218
-
219
- interface FileItem {
220
- path: string;
221
- relativePath: string;
222
- fileName: string;
223
- size: number;
224
- modified: number;
225
- gitStatus: string; // 'clean', 'modified', 'untracked', etc.
226
- }
227
- ```
228
-
229
- ## Grep Result Types
230
-
231
- ```typescript
232
- interface GrepResult {
233
- items: GrepMatch[];
234
- totalMatched: number;
235
- totalFilesSearched: number;
236
- totalFiles: number;
237
- filteredFileCount: number;
238
- nextCursor: GrepCursor | null; // Pass to options.cursor for next page
239
- regexFallbackError?: string; // Set if regex was invalid
240
- }
241
-
242
- interface GrepMatch {
243
- path: string;
244
- relativePath: string;
245
- fileName: string;
246
- gitStatus: string;
247
- lineNumber: number; // 1-based
248
- col: number; // 0-based byte column
249
- byteOffset: number; // Absolute byte offset in file
250
- lineContent: string; // The matched line text
251
- matchRanges: [number, number][]; // Byte offsets for highlighting
252
- fuzzyScore?: number; // Only in fuzzy mode
253
- }
254
- ```
99
+ This SDK calls a native compiled library for your platform at runtime. This is generally safe — fff is battle-tested and stable, and written in a memory-safe language — but there is a class of errors that can't be caught at the Bun/Node level. If you hit one, please report an issue!
255
100
 
256
101
  ## Building from Source
257
102
 
@@ -268,7 +113,7 @@ cargo build --release -p fff-c
268
113
  # The binary will be at target/release/libfff_c.{so,dylib,dll}
269
114
  ```
270
115
 
271
- ## CLI Tools
116
+ ## CLI examples
272
117
 
273
118
  ```bash
274
119
  # Download binary manually (fallback if npm package unavailable)
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Glob benchmark: fff.glob vs Bun.Glob vs npm `glob`.
4
+ *
5
+ * Each engine is asked to enumerate files in a directory matching the same
6
+ * pattern. We measure wall-clock time + result count. fff scans + indexes
7
+ * once on init; the subsequent glob call is a filter over the in-memory
8
+ * index — that's what we time.
9
+ *
10
+ * Usage:
11
+ * bun examples/glob-bench.ts [dir] [pattern] [iterations]
12
+ *
13
+ * dir default: cwd
14
+ * pattern default: "**\/*.ts"
15
+ * iterations default: 5 (each engine runs N times, best+median reported)
16
+ *
17
+ * Install npm glob first:
18
+ * bun add glob
19
+ */
20
+
21
+ import { performance } from "node:perf_hooks";
22
+ import { resolve } from "node:path";
23
+ import { Glob as BunGlob } from "bun";
24
+ import { FileFinder } from "../src/index";
25
+
26
+ // npm glob — optional. Skip silently if not installed.
27
+ let npmGlob:
28
+ | ((pattern: string, opts: { cwd: string }) => Promise<string[]>)
29
+ | null = null;
30
+ try {
31
+ const mod: {
32
+ glob: (pattern: string, opts: { cwd: string }) => Promise<string[]>;
33
+ } =
34
+ // @ts-ignore - optional peer; resolved at runtime, may be absent
35
+ await import("glob");
36
+ npmGlob = mod.glob;
37
+ } catch {
38
+ console.warn("npm `glob` not installed — skipping. Run: bun add glob");
39
+ }
40
+
41
+ const dir = resolve(process.argv[2] ?? process.cwd());
42
+ const pattern = process.argv[3] ?? "**/lua/**/*.lua";
43
+ const iterations = Number(process.argv[4] ?? 5);
44
+
45
+ console.log(`dir: ${dir}`);
46
+ console.log(`pattern: ${pattern}`);
47
+ console.log(`iterations: ${iterations}\n`);
48
+
49
+ interface Sample {
50
+ ms: number;
51
+ count: number;
52
+ }
53
+
54
+ function summarize(label: string, samples: Sample[]): void {
55
+ if (samples.length === 0) {
56
+ console.log(`${label.padEnd(16)} skipped`);
57
+ return;
58
+ }
59
+ const sorted = [...samples].sort((a, b) => a.ms - b.ms);
60
+ const best = sorted[0]!;
61
+ const median = sorted[Math.floor(sorted.length / 2)]!;
62
+ const worst = sorted[sorted.length - 1]!;
63
+ const counts = new Set(samples.map((s) => s.count));
64
+ const countStr =
65
+ counts.size === 1 ? `${best.count}` : `[${[...counts].join(", ")}]`;
66
+ console.log(
67
+ `${label.padEnd(16)} best=${best.ms.toFixed(2)}ms median=${median.ms.toFixed(2)}ms worst=${worst.ms.toFixed(2)}ms count=${countStr}`,
68
+ );
69
+ }
70
+
71
+ async function bench<T>(
72
+ fn: () => Promise<T> | T,
73
+ ): Promise<{ ms: number; result: T }> {
74
+ const start = performance.now();
75
+ const result = await fn();
76
+ return { ms: performance.now() - start, result };
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // fff: init + warm scan, then time only the .glob() call. Init cost is
81
+ // reported separately because it's amortized across many subsequent calls.
82
+ // ---------------------------------------------------------------------------
83
+ const fffInit = await bench(() => {
84
+ const result = FileFinder.create({
85
+ basePath: dir,
86
+ disableMmapCache: true,
87
+ disableContentIndexing: true,
88
+ disableWatch: true,
89
+ });
90
+ if (!result.ok) throw new Error(result.error);
91
+ return result.value;
92
+ });
93
+ const finder = fffInit.result;
94
+
95
+ // Wait until initial scan done so the first .glob() doesn't see a partial
96
+ // index. Returns true = completed, false = timed out.
97
+ const scanReady = finder.waitForScanBlocking(30_000);
98
+ if (!scanReady.ok || !scanReady.value) {
99
+ console.error("fff: initial scan did not finish in 30s — exiting");
100
+ process.exit(1);
101
+ }
102
+ console.log(`fff init+scan: ${fffInit.ms.toFixed(2)}ms\n`);
103
+
104
+ const fffSamples: Sample[] = [];
105
+ for (let i = 0; i < iterations; i++) {
106
+ const r = await bench(() => {
107
+ const out = finder.glob(pattern, { pageSize: 100 });
108
+ if (!out.ok) throw new Error(out.error);
109
+ return out.value;
110
+ });
111
+ fffSamples.push({ ms: r.ms, count: r.result.items.length });
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Bun.Glob — sync iterator, returns relative paths.
116
+ // ---------------------------------------------------------------------------
117
+ const bunSamples: Sample[] = [];
118
+ for (let i = 0; i < iterations; i++) {
119
+ const r = await bench(() => {
120
+ const g = new BunGlob(pattern);
121
+ let count = 0;
122
+ for (const _ of g.scanSync({ cwd: dir })) count++;
123
+ return count;
124
+ });
125
+ bunSamples.push({ ms: r.ms, count: r.result });
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // npm glob — async, returns absolute or relative paths depending on opts.
130
+ // ---------------------------------------------------------------------------
131
+ const npmSamples: Sample[] = [];
132
+ if (npmGlob) {
133
+ for (let i = 0; i < iterations; i++) {
134
+ const r = await bench(() => npmGlob!(pattern, { cwd: dir }));
135
+ npmSamples.push({ ms: r.ms, count: r.result.length });
136
+ }
137
+ }
138
+
139
+ console.log("results:");
140
+ summarize("fff.glob", fffSamples);
141
+ summarize("Bun.Glob", bunSamples);
142
+ summarize("npm glob", npmSamples);
143
+
144
+ // Sanity: counts should be in the same ballpark. They won't match exactly
145
+ // because indexing rules differ (fff respects gitignore + skips binaries by
146
+ // default; Bun.Glob and npm glob do not).
147
+ const counts = {
148
+ fff: fffSamples[0]?.count ?? 0,
149
+ bun: bunSamples[0]?.count ?? 0,
150
+ npm: npmSamples[0]?.count ?? 0,
151
+ };
152
+ console.log(
153
+ `\nNote: fff respects gitignore + skips binaries; Bun.Glob and npm glob walk the raw filesystem. Count differences are expected.`,
154
+ );
155
+ console.log(
156
+ `raw counts: fff=${counts.fff} bun=${counts.bun} npm=${counts.npm}`,
157
+ );
158
+
159
+ finder.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ff-labs/fff-bun",
3
- "version": "0.8.5-nightly.ccb1b9d",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
6
6
  "type": "module",
@@ -62,14 +62,14 @@
62
62
  },
63
63
  "homepage": "https://github.com/dmtrKovalenko/fff#readme",
64
64
  "optionalDependencies": {
65
- "@ff-labs/fff-bin-darwin-arm64": "0.8.5-nightly.ccb1b9d",
66
- "@ff-labs/fff-bin-darwin-x64": "0.8.5-nightly.ccb1b9d",
67
- "@ff-labs/fff-bin-linux-x64-gnu": "0.8.5-nightly.ccb1b9d",
68
- "@ff-labs/fff-bin-linux-arm64-gnu": "0.8.5-nightly.ccb1b9d",
69
- "@ff-labs/fff-bin-linux-x64-musl": "0.8.5-nightly.ccb1b9d",
70
- "@ff-labs/fff-bin-linux-arm64-musl": "0.8.5-nightly.ccb1b9d",
71
- "@ff-labs/fff-bin-win32-x64": "0.8.5-nightly.ccb1b9d",
72
- "@ff-labs/fff-bin-win32-arm64": "0.8.5-nightly.ccb1b9d"
65
+ "@ff-labs/fff-bin-darwin-arm64": "0.9.0",
66
+ "@ff-labs/fff-bin-darwin-x64": "0.9.0",
67
+ "@ff-labs/fff-bin-linux-x64-gnu": "0.9.0",
68
+ "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.0",
69
+ "@ff-labs/fff-bin-linux-x64-musl": "0.9.0",
70
+ "@ff-labs/fff-bin-linux-arm64-musl": "0.9.0",
71
+ "@ff-labs/fff-bin-win32-x64": "0.9.0",
72
+ "@ff-labs/fff-bin-win32-arm64": "0.9.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/bun": "^1.3.8",