@ff-labs/fff-bun 0.1.0-nightly.044314f

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 ADDED
@@ -0,0 +1,283 @@
1
+ # fff - Fast File Finder
2
+
3
+ High-performance fuzzy file finder for Bun, powered by Rust. Perfect for LLM agent tools that need to search through codebases.
4
+
5
+ ## Features
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
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ bun add @ff-labs/bun
17
+ ```
18
+
19
+ The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bun-darwin-arm64`, `@ff-labs/fff-bun-linux-x64-gnu`). No GitHub downloads are needed.
20
+
21
+ ### Supported Platforms
22
+
23
+ | Platform | Architecture | Package |
24
+ |----------|-------------|---------|
25
+ | macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bun-darwin-arm64` |
26
+ | macOS | x64 (Intel) | `@ff-labs/fff-bun-darwin-x64` |
27
+ | Linux | x64 (glibc) | `@ff-labs/fff-bun-linux-x64-gnu` |
28
+ | Linux | ARM64 (glibc) | `@ff-labs/fff-bun-linux-arm64-gnu` |
29
+ | Linux | x64 (musl) | `@ff-labs/fff-bun-linux-x64-musl` |
30
+ | Linux | ARM64 (musl) | `@ff-labs/fff-bun-linux-arm64-musl` |
31
+ | Windows | x64 | `@ff-labs/fff-bun-win32-x64` |
32
+ | Windows | ARM64 | `@ff-labs/fff-bun-win32-arm64` |
33
+
34
+ If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback.
35
+
36
+ ## Quick Start
37
+
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.
110
+
111
+ ```typescript
112
+ // Call when user opens a file
113
+ FileFinder.trackAccess("/path/to/file.ts");
114
+ ```
115
+
116
+ ### `FileFinder.liveGrep(query, options?)`
117
+
118
+ Search file contents with SIMD-accelerated matching.
119
+
120
+ ```typescript
121
+ interface GrepOptions {
122
+ maxFileSize?: number; // Max file size in bytes (default: 10MB)
123
+ maxMatchesPerFile?: number; // Max matches per file (default: 200)
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
+ }
130
+
131
+ // Plain text search
132
+ const result = FileFinder.liveGrep("TODO", { pageLimit: 20 });
133
+ if (result.ok) {
134
+ for (const match of result.value.items) {
135
+ console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
136
+ }
137
+ }
138
+
139
+ // Regex search
140
+ const regexResult = FileFinder.liveGrep("fn\\s+\\w+", { mode: "regex" });
141
+
142
+ // Fuzzy search
143
+ const fuzzyResult = FileFinder.liveGrep("imprt recat", { mode: "fuzzy" });
144
+
145
+ // Pagination
146
+ const page1 = FileFinder.liveGrep("error");
147
+ if (page1.ok && page1.value.nextCursor) {
148
+ const page2 = FileFinder.liveGrep("error", {
149
+ cursor: page1.value.nextCursor,
150
+ });
151
+ }
152
+
153
+ // With file constraints
154
+ const tsOnly = FileFinder.liveGrep("*.ts useState");
155
+ const srcOnly = FileFinder.liveGrep("src/ handleClick");
156
+ ```
157
+
158
+ ### `FileFinder.trackQuery(query, selectedFile)`
159
+
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");
165
+ ```
166
+
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
+
179
+ ### Other Methods
180
+
181
+ - `FileFinder.liveGrep(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
190
+
191
+ ## Result Types
192
+
193
+ All methods return a `Result<T>` type for explicit error handling:
194
+
195
+ ```typescript
196
+ type Result<T> =
197
+ | { ok: true; value: T }
198
+ | { ok: false; error: string };
199
+
200
+ const result = FileFinder.search("foo");
201
+ if (result.ok) {
202
+ // result.value is SearchResult
203
+ } else {
204
+ // result.error is string
205
+ }
206
+ ```
207
+
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
+ ```
255
+
256
+ ## Building from Source
257
+
258
+ If prebuilt binaries aren't available for your platform:
259
+
260
+ ```bash
261
+ # Clone the repository
262
+ git clone https://github.com/dmtrKovalenko/fff.nvim
263
+ cd fff.nvim
264
+
265
+ # Build the C library
266
+ cargo build --release -p fff-c
267
+
268
+ # The binary will be at target/release/libfff_c.{so,dylib,dll}
269
+ ```
270
+
271
+ ## CLI Tools
272
+
273
+ ```bash
274
+ # Download binary manually (fallback if npm package unavailable)
275
+ bunx fff download [tag]
276
+
277
+ # Show platform info and binary location
278
+ bunx fff info
279
+ ```
280
+
281
+ ## License
282
+
283
+ MIT
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Interactive live grep demo
4
+ *
5
+ * Usage:
6
+ * bun examples/grep.ts [directory] [--mode=plain|regex|fuzzy]
7
+ *
8
+ * Indexes the specified directory (or cwd) and provides an interactive
9
+ * content search prompt with match highlighting.
10
+ */
11
+
12
+ import { FileFinder } from "../src/index";
13
+ import type { GrepMode } from "../src/types";
14
+ import * as readline from "readline";
15
+
16
+ const RESET = "\x1b[0m";
17
+ const BOLD = "\x1b[1m";
18
+ const DIM = "\x1b[2m";
19
+ const GREEN = "\x1b[32m";
20
+ const YELLOW = "\x1b[33m";
21
+ const BLUE = "\x1b[34m";
22
+ const CYAN = "\x1b[36m";
23
+ const RED = "\x1b[31m";
24
+ const BG_YELLOW = "\x1b[43m";
25
+ const BLACK = "\x1b[30m";
26
+
27
+ function formatGitStatus(status: string): string {
28
+ switch (status) {
29
+ case "modified":
30
+ return `${YELLOW}M${RESET}`;
31
+ case "untracked":
32
+ return `${GREEN}?${RESET}`;
33
+ case "added":
34
+ return `${GREEN}A${RESET}`;
35
+ case "deleted":
36
+ return `${RED}D${RESET}`;
37
+ case "renamed":
38
+ return `${BLUE}R${RESET}`;
39
+ case "clear":
40
+ case "current":
41
+ return `${DIM} ${RESET}`;
42
+ default:
43
+ return `${DIM}${status.charAt(0)}${RESET}`;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Highlight match ranges within line content using ANSI escape codes.
49
+ * The match_ranges are byte offsets into line_content.
50
+ */
51
+ function highlightLine(content: string, ranges: [number, number][]): string {
52
+ if (ranges.length === 0) return content;
53
+
54
+ // Convert the string to a buffer to work with byte offsets
55
+ const buf = Buffer.from(content, "utf-8");
56
+ const parts: string[] = [];
57
+ let lastEnd = 0;
58
+
59
+ for (const [start, end] of ranges) {
60
+ // Clamp to valid range
61
+ const s = Math.max(0, Math.min(start, buf.length));
62
+ const e = Math.max(s, Math.min(end, buf.length));
63
+
64
+ if (s > lastEnd) {
65
+ parts.push(buf.subarray(lastEnd, s).toString("utf-8"));
66
+ }
67
+ parts.push(
68
+ `${BG_YELLOW}${BLACK}${buf.subarray(s, e).toString("utf-8")}${RESET}`
69
+ );
70
+ lastEnd = e;
71
+ }
72
+
73
+ if (lastEnd < buf.length) {
74
+ parts.push(buf.subarray(lastEnd).toString("utf-8"));
75
+ }
76
+
77
+ return parts.join("");
78
+ }
79
+
80
+ function parseArgs(): { directory: string; mode: GrepMode } {
81
+ let directory = process.cwd();
82
+ let mode: GrepMode = "plain";
83
+
84
+ for (const arg of process.argv.slice(2)) {
85
+ if (arg.startsWith("--mode=")) {
86
+ const m = arg.slice(7);
87
+ if (m === "plain" || m === "regex" || m === "fuzzy") {
88
+ mode = m;
89
+ } else {
90
+ console.error(`Unknown mode: ${m}. Use plain, regex, or fuzzy.`);
91
+ process.exit(1);
92
+ }
93
+ } else if (!arg.startsWith("-")) {
94
+ directory = arg;
95
+ }
96
+ }
97
+
98
+ return { directory, mode };
99
+ }
100
+
101
+ async function main() {
102
+ const { directory, mode } = parseArgs();
103
+
104
+ console.log(`${BOLD}${CYAN}fff - Live Grep Demo${RESET}`);
105
+ console.log(`${DIM}Mode: ${mode}${RESET}\n`);
106
+
107
+ if (!FileFinder.isAvailable()) {
108
+ console.error(`${RED}Error: Native library not found.${RESET}`);
109
+ console.error("Build with: cargo build --release -p fff-c");
110
+ process.exit(1);
111
+ }
112
+
113
+ console.log(`${DIM}Initializing index for: ${directory}${RESET}`);
114
+ const initResult = FileFinder.init({
115
+ basePath: directory,
116
+ warmupMmapCache: true,
117
+ });
118
+
119
+ if (!initResult.ok) {
120
+ console.error(`${RED}Init failed: ${initResult.error}${RESET}`);
121
+ process.exit(1);
122
+ }
123
+
124
+ // Wait for scan
125
+ process.stdout.write(`${DIM}Scanning files...${RESET}`);
126
+ const startTime = Date.now();
127
+
128
+ while (FileFinder.isScanning()) {
129
+ const progress = FileFinder.getScanProgress();
130
+ if (progress.ok) {
131
+ process.stdout.write(
132
+ `\r${DIM}Scanning files... ${progress.value.scannedFilesCount}${RESET} `
133
+ );
134
+ }
135
+ await new Promise((r) => setTimeout(r, 50));
136
+ }
137
+
138
+ const scanTime = Date.now() - startTime;
139
+ const finalProgress = FileFinder.getScanProgress();
140
+ const totalFiles = finalProgress.ok
141
+ ? finalProgress.value.scannedFilesCount
142
+ : 0;
143
+
144
+ console.log(
145
+ `\r${GREEN}✓${RESET} Indexed ${BOLD}${totalFiles}${RESET} files in ${scanTime}ms\n`
146
+ );
147
+
148
+ console.log(
149
+ `${BOLD}Enter a search pattern${RESET} (or 'q' to quit, ':mode plain|regex|fuzzy' to switch):\n`
150
+ );
151
+ console.log(
152
+ `${DIM}Tip: prefix with *.ext to filter by extension, e.g. "*.ts useState"${RESET}\n`
153
+ );
154
+
155
+ let currentMode = mode;
156
+
157
+ const rl = readline.createInterface({
158
+ input: process.stdin,
159
+ output: process.stdout,
160
+ });
161
+
162
+ const prompt = () => {
163
+ const modeLabel =
164
+ currentMode === "plain" ? "txt" : currentMode === "regex" ? "re" : "fzy";
165
+
166
+ rl.question(`${CYAN}grep[${modeLabel}]>${RESET} `, (query) => {
167
+ if (query.toLowerCase() === "q" || query.toLowerCase() === "quit") {
168
+ console.log(`\n${DIM}Goodbye!${RESET}`);
169
+ FileFinder.destroy();
170
+ rl.close();
171
+ process.exit(0);
172
+ }
173
+
174
+ // Handle mode switching
175
+ if (query.startsWith(":mode ")) {
176
+ const newMode = query.slice(6).trim();
177
+ if (
178
+ newMode === "plain" ||
179
+ newMode === "regex" ||
180
+ newMode === "fuzzy"
181
+ ) {
182
+ currentMode = newMode;
183
+ console.log(`${DIM}Switched to ${currentMode} mode${RESET}\n`);
184
+ } else {
185
+ console.log(
186
+ `${RED}Unknown mode: ${newMode}. Use plain, regex, or fuzzy.${RESET}\n`
187
+ );
188
+ }
189
+ prompt();
190
+ return;
191
+ }
192
+
193
+ if (query.trim() === "") {
194
+ prompt();
195
+ return;
196
+ }
197
+
198
+ const searchStart = Date.now();
199
+ const result = FileFinder.liveGrep(query, {
200
+ mode: currentMode,
201
+ pageLimit: 30,
202
+ timeBudgetMs: 5000,
203
+ });
204
+ const searchTime = Date.now() - searchStart;
205
+
206
+ if (!result.ok) {
207
+ console.log(`${RED}Grep error: ${result.error}${RESET}\n`);
208
+ prompt();
209
+ return;
210
+ }
211
+
212
+ const {
213
+ items,
214
+ totalMatched,
215
+ totalFilesSearched,
216
+ totalFiles: indexedFiles,
217
+ filteredFileCount,
218
+ nextCursor,
219
+ regexFallbackError,
220
+ } = result.value;
221
+
222
+ console.log();
223
+
224
+ if (regexFallbackError) {
225
+ console.log(
226
+ `${YELLOW}Regex error: ${regexFallbackError} (fell back to literal match)${RESET}`
227
+ );
228
+ }
229
+
230
+ console.log(
231
+ `${DIM}${BOLD}${totalMatched}${RESET}${DIM} matches across ${totalFilesSearched}/${filteredFileCount} files (${searchTime}ms)${RESET}`
232
+ );
233
+ console.log();
234
+
235
+ if (items.length === 0) {
236
+ console.log(`${DIM}No matches found.${RESET}\n`);
237
+ prompt();
238
+ return;
239
+ }
240
+
241
+ // Group matches by file for display
242
+ let lastFile = "";
243
+ for (const match of items) {
244
+ if (match.relativePath !== lastFile) {
245
+ lastFile = match.relativePath;
246
+ const git = formatGitStatus(match.gitStatus);
247
+ console.log(
248
+ `${BOLD}${BLUE}${match.relativePath}${RESET} ${git}`
249
+ );
250
+ }
251
+
252
+ const lineNum = String(match.lineNumber).padStart(4);
253
+ const highlighted = highlightLine(
254
+ match.lineContent,
255
+ match.matchRanges
256
+ );
257
+
258
+ let suffix = "";
259
+ if (match.fuzzyScore !== undefined) {
260
+ suffix = ` ${DIM}(score: ${match.fuzzyScore})${RESET}`;
261
+ }
262
+
263
+ console.log(`${DIM}${lineNum}:${RESET} ${highlighted}${suffix}`);
264
+ }
265
+
266
+ if (nextCursor) {
267
+ console.log(
268
+ `\n${DIM}... more results available${RESET}`
269
+ );
270
+ }
271
+
272
+ console.log();
273
+ prompt();
274
+ });
275
+ };
276
+
277
+ prompt();
278
+ }
279
+
280
+ main().catch((err) => {
281
+ console.error(`${RED}Fatal error: ${err.message}${RESET}`);
282
+ process.exit(1);
283
+ });