@ff-labs/fff-bun 0.1.0-nightly.f69dda4 → 0.2.4-dev.233679d

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
@@ -113,7 +113,7 @@ Track file access for frecency scoring.
113
113
  FileFinder.trackAccess("/path/to/file.ts");
114
114
  ```
115
115
 
116
- ### `FileFinder.liveGrep(query, options?)`
116
+ ### `FileFinder.grep(query, options?)`
117
117
 
118
118
  Search file contents with SIMD-accelerated matching.
119
119
 
@@ -129,7 +129,7 @@ interface GrepOptions {
129
129
  }
130
130
 
131
131
  // Plain text search
132
- const result = FileFinder.liveGrep("TODO", { pageLimit: 20 });
132
+ const result = FileFinder.grep("TODO", { pageLimit: 20 });
133
133
  if (result.ok) {
134
134
  for (const match of result.value.items) {
135
135
  console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
@@ -137,22 +137,22 @@ if (result.ok) {
137
137
  }
138
138
 
139
139
  // Regex search
140
- const regexResult = FileFinder.liveGrep("fn\\s+\\w+", { mode: "regex" });
140
+ const regexResult = FileFinder.grep("fn\\s+\\w+", { mode: "regex" });
141
141
 
142
142
  // Fuzzy search
143
- const fuzzyResult = FileFinder.liveGrep("imprt recat", { mode: "fuzzy" });
143
+ const fuzzyResult = FileFinder.grep("imprt recat", { mode: "fuzzy" });
144
144
 
145
145
  // Pagination
146
- const page1 = FileFinder.liveGrep("error");
146
+ const page1 = FileFinder.grep("error");
147
147
  if (page1.ok && page1.value.nextCursor) {
148
- const page2 = FileFinder.liveGrep("error", {
148
+ const page2 = FileFinder.grep("error", {
149
149
  cursor: page1.value.nextCursor,
150
150
  });
151
151
  }
152
152
 
153
153
  // With file constraints
154
- const tsOnly = FileFinder.liveGrep("*.ts useState");
155
- const srcOnly = FileFinder.liveGrep("src/ handleClick");
154
+ const tsOnly = FileFinder.grep("*.ts useState");
155
+ const srcOnly = FileFinder.grep("src/ handleClick");
156
156
  ```
157
157
 
158
158
  ### `FileFinder.trackQuery(query, selectedFile)`
@@ -178,7 +178,7 @@ if (health.ok) {
178
178
 
179
179
  ### Other Methods
180
180
 
181
- - `FileFinder.liveGrep(query, options?)` - Search file contents
181
+ - `FileFinder.grep(query, options?)` - Search file contents
182
182
  - `FileFinder.scanFiles()` - Trigger rescan
183
183
  - `FileFinder.isScanning()` - Check scan status
184
184
  - `FileFinder.getScanProgress()` - Get scan progress
package/examples/grep.ts CHANGED
@@ -190,7 +190,7 @@ async function main() {
190
190
  }
191
191
 
192
192
  const searchStart = Date.now();
193
- const result = finder.liveGrep(query, {
193
+ const result = finder.grep(query, {
194
194
  mode: currentMode,
195
195
  pageLimit: 30,
196
196
  timeBudgetMs: 5000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ff-labs/fff-bun",
3
- "version": "0.1.0-nightly.f69dda4",
3
+ "version": "0.2.4-dev.233679d",
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.nvim#readme",
64
64
  "optionalDependencies": {
65
- "@ff-labs/fff-bin-darwin-arm64": "0.1.0-nightly.f69dda4",
66
- "@ff-labs/fff-bin-darwin-x64": "0.1.0-nightly.f69dda4",
67
- "@ff-labs/fff-bin-linux-x64-gnu": "0.1.0-nightly.f69dda4",
68
- "@ff-labs/fff-bin-linux-arm64-gnu": "0.1.0-nightly.f69dda4",
69
- "@ff-labs/fff-bin-linux-x64-musl": "0.1.0-nightly.f69dda4",
70
- "@ff-labs/fff-bin-linux-arm64-musl": "0.1.0-nightly.f69dda4",
71
- "@ff-labs/fff-bin-win32-x64": "0.1.0-nightly.f69dda4",
72
- "@ff-labs/fff-bin-win32-arm64": "0.1.0-nightly.f69dda4"
65
+ "@ff-labs/fff-bin-darwin-arm64": "0.2.4-dev.233679d",
66
+ "@ff-labs/fff-bin-darwin-x64": "0.2.4-dev.233679d",
67
+ "@ff-labs/fff-bin-linux-x64-gnu": "0.2.4-dev.233679d",
68
+ "@ff-labs/fff-bin-linux-arm64-gnu": "0.2.4-dev.233679d",
69
+ "@ff-labs/fff-bin-linux-x64-musl": "0.2.4-dev.233679d",
70
+ "@ff-labs/fff-bin-linux-arm64-musl": "0.2.4-dev.233679d",
71
+ "@ff-labs/fff-bin-win32-x64": "0.2.4-dev.233679d",
72
+ "@ff-labs/fff-bin-win32-arm64": "0.2.4-dev.233679d"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/bun": "^1.3.8",
package/src/ffi.ts CHANGED
@@ -10,14 +10,36 @@
10
10
 
11
11
  import { CString, dlopen, FFIType, type Pointer, ptr, read } from "bun:ffi";
12
12
  import { findBinary } from "./download";
13
- import type { Result } from "./types";
13
+ import type { FileItem, Location, Result, Score, SearchResult } from "./types";
14
14
  import { err } from "./types";
15
15
 
16
- // Define the FFI symbols
16
+ /** Grep mode constants matching the C API (u8). */
17
+ const GREP_MODE_PLAIN = 0;
18
+ const GREP_MODE_REGEX = 1;
19
+ const GREP_MODE_FUZZY = 2;
20
+
21
+ /** Map string mode to u8 */
22
+ function grepModeToU8(mode?: string): number {
23
+ switch (mode) {
24
+ case "regex":
25
+ return GREP_MODE_REGEX;
26
+ case "fuzzy":
27
+ return GREP_MODE_FUZZY;
28
+ default:
29
+ return GREP_MODE_PLAIN;
30
+ }
31
+ }
32
+
17
33
  const ffiDefinition = {
18
- // Lifecycle
19
- fff_create: {
20
- args: [FFIType.cstring],
34
+ fff_create_instance: {
35
+ args: [
36
+ FFIType.cstring, // base_path
37
+ FFIType.cstring, // frecency_db_path
38
+ FFIType.cstring, // history_db_path
39
+ FFIType.bool, // use_unsafe_no_lock
40
+ FFIType.bool, // warmup_mmap_cache
41
+ FFIType.bool, // ai_mode
42
+ ],
21
43
  returns: FFIType.ptr,
22
44
  },
23
45
  fff_destroy: {
@@ -27,19 +49,54 @@ const ffiDefinition = {
27
49
 
28
50
  // Search
29
51
  fff_search: {
30
- args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
52
+ args: [
53
+ FFIType.ptr, // handle
54
+ FFIType.cstring, // query
55
+ FFIType.cstring, // current_file
56
+ FFIType.u32, // max_threads
57
+ FFIType.u32, // page_index
58
+ FFIType.u32, // page_size
59
+ FFIType.i32, // combo_boost_multiplier
60
+ FFIType.u32, // min_combo_count
61
+ ],
31
62
  returns: FFIType.ptr,
32
63
  },
33
64
 
34
65
  // Live grep (content search)
35
66
  fff_live_grep: {
36
- args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
67
+ args: [
68
+ FFIType.ptr, // handle
69
+ FFIType.cstring, // query
70
+ FFIType.u8, // mode
71
+ FFIType.u64, // max_file_size
72
+ FFIType.u32, // max_matches_per_file
73
+ FFIType.bool, // smart_case
74
+ FFIType.u32, // file_offset
75
+ FFIType.u32, // page_limit
76
+ FFIType.u64, // time_budget_ms
77
+ FFIType.u32, // before_context
78
+ FFIType.u32, // after_context
79
+ FFIType.bool, // classify_definitions
80
+ ],
37
81
  returns: FFIType.ptr,
38
82
  },
39
83
 
40
84
  // Multi-pattern grep (Aho-Corasick)
41
85
  fff_multi_grep: {
42
- args: [FFIType.ptr, FFIType.cstring],
86
+ args: [
87
+ FFIType.ptr, // handle
88
+ FFIType.cstring, // patterns_joined (\n-separated)
89
+ FFIType.cstring, // constraints
90
+ FFIType.u64, // max_file_size
91
+ FFIType.u32, // max_matches_per_file
92
+ FFIType.bool, // smart_case
93
+ FFIType.u32, // file_offset
94
+ FFIType.u32, // page_limit
95
+ FFIType.u64, // time_budget_ms
96
+ FFIType.u32, // before_context
97
+ FFIType.u32, // after_context
98
+ FFIType.bool, // classify_definitions
99
+ ],
43
100
  returns: FFIType.ptr,
44
101
  },
45
102
 
@@ -87,6 +144,20 @@ const ffiDefinition = {
87
144
  returns: FFIType.ptr,
88
145
  },
89
146
 
147
+ // Search result accessors
148
+ fff_free_search_result: {
149
+ args: [FFIType.ptr],
150
+ returns: FFIType.void,
151
+ },
152
+ fff_search_result_get_item: {
153
+ args: [FFIType.ptr, FFIType.u32],
154
+ returns: FFIType.ptr,
155
+ },
156
+ fff_search_result_get_score: {
157
+ args: [FFIType.ptr, FFIType.u32],
158
+ returns: FFIType.ptr,
159
+ },
160
+
90
161
  // Memory management
91
162
  fff_free_result: {
92
163
  args: [FFIType.ptr],
@@ -210,13 +281,24 @@ export type NativeHandle = Pointer;
210
281
 
211
282
  /**
212
283
  * Create a new file finder instance.
213
- *
214
- * Returns the opaque native handle on success. The handle must be passed to
215
- * all subsequent FFI calls and freed with `ffiDestroy`.
216
284
  */
217
- export function ffiCreate(optsJson: string): Result<NativeHandle> {
285
+ export function ffiCreate(
286
+ basePath: string,
287
+ frecencyDbPath: string,
288
+ historyDbPath: string,
289
+ useUnsafeNoLock: boolean,
290
+ warmupMmapCache: boolean,
291
+ aiMode: boolean,
292
+ ): Result<NativeHandle> {
218
293
  const library = loadLibrary();
219
- const resultPtr = library.symbols.fff_create(ptr(encodeString(optsJson)));
294
+ const resultPtr = library.symbols.fff_create_instance(
295
+ ptr(encodeString(basePath)),
296
+ ptr(encodeString(frecencyDbPath)),
297
+ ptr(encodeString(historyDbPath)),
298
+ useUnsafeNoLock,
299
+ warmupMmapCache,
300
+ aiMode,
301
+ );
220
302
 
221
303
  if (resultPtr === null) {
222
304
  return err("FFI returned null pointer");
@@ -231,7 +313,7 @@ export function ffiCreate(optsJson: string): Result<NativeHandle> {
231
313
  library.symbols.fff_free_result(resultPtr);
232
314
 
233
315
  if (!handle || handle === (0 as unknown as Pointer)) {
234
- return err("fff_create returned null handle");
316
+ return err("fff_create_instance returned null handle");
235
317
  }
236
318
 
237
319
  return { ok: true, value: handle };
@@ -250,19 +332,254 @@ export function ffiDestroy(handle: NativeHandle): void {
250
332
  library.symbols.fff_destroy(handle);
251
333
  }
252
334
 
335
+ // ---------------------------------------------------------------------------
336
+ // Struct byte offsets (must match #[repr(C)] layout on 64-bit)
337
+ // ---------------------------------------------------------------------------
338
+
339
+ // FffSearchResult { items: *mut, scores: *mut, count: u32, total_matched: u32, total_files: u32, location: FffLocation }
340
+ const SR_ITEMS = 0; // *mut FffFileItem (8)
341
+ const SR_SCORES = 8; // *mut FffScore (8)
342
+ const SR_COUNT = 16; // u32 (4)
343
+ const SR_MATCHED = 20; // u32 (4)
344
+ const SR_TOTAL = 24; // u32 (4)
345
+ // FffLocation is inlined at offset 28
346
+ const SR_LOC_TAG = 28; // u8 (1 + 3 padding)
347
+ const SR_LOC_LINE = 32; // i32 (4)
348
+ const SR_LOC_COL = 36; // i32 (4)
349
+ const SR_LOC_END_LINE = 40; // i32 (4)
350
+ const SR_LOC_END_COL = 44; // i32 (4)
351
+
352
+ // FffFileItem (80 bytes)
353
+ const FI_PATH = 0; // *mut c_char (8)
354
+ const FI_RELPATH = 8; // *mut c_char (8)
355
+ const FI_FNAME = 16; // *mut c_char (8)
356
+ const FI_GIT = 24; // *mut c_char (8)
357
+ const FI_SIZE = 32; // u64 (8)
358
+ const FI_MODIFIED = 40; // u64 (8)
359
+ const FI_ACCESS = 48; // i64 (8)
360
+ const FI_MODFR = 56; // i64 (8)
361
+ const FI_TOTAL_FR = 64; // i64 (8)
362
+ const FI_BINARY = 72; // bool (1 + 7 pad)
363
+ const FI_SIZE_OF = 80;
364
+
365
+ // FffScore (48 bytes)
366
+ const SC_TOTAL = 0; // i32 (4)
367
+ const SC_BASE = 4; // i32 (4)
368
+ const SC_FNAME = 8; // i32 (4)
369
+ const SC_SPECIAL = 12; // i32 (4)
370
+ const SC_FREC = 16; // i32 (4)
371
+ const SC_DIST = 20; // i32 (4)
372
+ const SC_CURFILE = 24; // i32 (4)
373
+ const SC_COMBO = 28; // i32 (4)
374
+ const SC_EXACT = 32; // bool (1 + 7 pad)
375
+ const SC_MTYPE = 40; // *mut c_char (8)
376
+ const SC_SIZE_OF = 48;
377
+
378
+ /** Cast a number (raw address from pointer math) to Pointer for read.*. */
379
+ function asPtr(n: number): Pointer {
380
+ return n as unknown as Pointer;
381
+ }
382
+
383
+ /**
384
+ * Read an FffFileItem struct at the given raw address.
385
+ */
386
+ function readFileItemStruct(p: number): FileItem {
387
+ const pp = asPtr(p);
388
+ return {
389
+ path: readCString(read.ptr(pp, FI_PATH)) ?? "",
390
+ relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
391
+ fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
392
+ gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
393
+ size: Number(read.u64(pp, FI_SIZE)),
394
+ modified: Number(read.u64(pp, FI_MODIFIED)),
395
+ accessFrecencyScore: Number(read.i64(pp, FI_ACCESS)),
396
+ modificationFrecencyScore: Number(read.i64(pp, FI_MODFR)),
397
+ totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Read an FffScore struct at the given raw address.
403
+ */
404
+ function readScoreStruct(p: number): Score {
405
+ const pp = asPtr(p);
406
+ return {
407
+ total: read.i32(pp, SC_TOTAL),
408
+ baseScore: read.i32(pp, SC_BASE),
409
+ filenameBonus: read.i32(pp, SC_FNAME),
410
+ specialFilenameBonus:read.i32(pp, SC_SPECIAL),
411
+ frecencyBoost: read.i32(pp, SC_FREC),
412
+ distancePenalty: read.i32(pp, SC_DIST),
413
+ currentFilePenalty: read.i32(pp, SC_CURFILE),
414
+ comboMatchBoost: read.i32(pp, SC_COMBO),
415
+ exactMatch: read.u8(pp, SC_EXACT) !== 0,
416
+ matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Parse an FffSearchResult from a raw pointer, then free native memory.
422
+ */
423
+ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
424
+ if (resultPtr === null) {
425
+ return err("FFI returned null pointer");
426
+ }
427
+
428
+ const success = read.u8(resultPtr, 0) !== 0;
429
+ const errorPtr = read.ptr(resultPtr, 16);
430
+ const handlePtr = read.ptr(resultPtr, 24);
431
+
432
+ const library = loadLibrary();
433
+
434
+ if (!success) {
435
+ const errorMsg = readCString(errorPtr) || "Unknown error";
436
+ library.symbols.fff_free_result(resultPtr);
437
+ return err(errorMsg);
438
+ }
439
+
440
+ // Free the FffResult envelope (does NOT free handle)
441
+ library.symbols.fff_free_result(resultPtr);
442
+
443
+ if (handlePtr === 0) {
444
+ return err("fff_search returned null search result");
445
+ }
446
+
447
+ // Read FffSearchResult struct from handlePtr
448
+ // Cast number to Pointer for Bun's read.* type requirements
449
+ const hp = handlePtr as unknown as Pointer;
450
+ const count = read.u32(hp, SR_COUNT);
451
+ const totalMatched = read.u32(hp, SR_MATCHED);
452
+ const totalFiles = read.u32(hp, SR_TOTAL);
453
+ const itemsBase = read.ptr(hp, SR_ITEMS);
454
+ const scoresBase = read.ptr(hp, SR_SCORES);
455
+
456
+ // Read location
457
+ const locTag = read.u8(hp, SR_LOC_TAG);
458
+ let location: Location | undefined;
459
+ if (locTag === 1) {
460
+ location = { type: "line", line: read.i32(hp, SR_LOC_LINE) };
461
+ } else if (locTag === 2) {
462
+ location = { type: "position", line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) };
463
+ } else if (locTag === 3) {
464
+ location = {
465
+ type: "range",
466
+ start: { line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) },
467
+ end: { line: read.i32(hp, SR_LOC_END_LINE), col: read.i32(hp, SR_LOC_END_COL) },
468
+ };
469
+ }
470
+
471
+ // Read items and scores arrays using pointer arithmetic
472
+ const items: FileItem[] = [];
473
+ const scores: Score[] = [];
474
+
475
+ for (let i = 0; i < count; i++) {
476
+ items.push(readFileItemStruct(itemsBase + i * FI_SIZE_OF));
477
+ scores.push(readScoreStruct(scoresBase + i * SC_SIZE_OF));
478
+ }
479
+
480
+ // Free native search result
481
+ library.symbols.fff_free_search_result(hp);
482
+
483
+ const result: SearchResult = { items, scores, totalMatched, totalFiles };
484
+ if (location) {
485
+ result.location = location;
486
+ }
487
+ return { ok: true, value: result };
488
+ }
489
+
253
490
  /**
254
491
  * Perform fuzzy search.
255
492
  */
256
493
  export function ffiSearch(
257
494
  handle: NativeHandle,
258
495
  query: string,
259
- optsJson: string,
260
- ): Result<unknown> {
496
+ currentFile: string,
497
+ maxThreads: number,
498
+ pageIndex: number,
499
+ pageSize: number,
500
+ comboBoostMultiplier: number,
501
+ minComboCount: number,
502
+ ): Result<SearchResult> {
261
503
  const library = loadLibrary();
262
504
  const resultPtr = library.symbols.fff_search(
263
505
  handle,
264
506
  ptr(encodeString(query)),
265
- ptr(encodeString(optsJson)),
507
+ ptr(encodeString(currentFile)),
508
+ maxThreads,
509
+ pageIndex,
510
+ pageSize,
511
+ comboBoostMultiplier,
512
+ minComboCount,
513
+ );
514
+ return parseSearchResult(resultPtr);
515
+ }
516
+
517
+ /**
518
+ * Live grep - search file contents.
519
+ */
520
+ export function ffiLiveGrep(
521
+ handle: NativeHandle,
522
+ query: string,
523
+ mode: string,
524
+ maxFileSize: number,
525
+ maxMatchesPerFile: number,
526
+ smartCase: boolean,
527
+ fileOffset: number,
528
+ pageLimit: number,
529
+ timeBudgetMs: number,
530
+ beforeContext: number,
531
+ afterContext: number,
532
+ classifyDefinitions: boolean,
533
+ ): Result<unknown> {
534
+ const library = loadLibrary();
535
+ const resultPtr = library.symbols.fff_live_grep(
536
+ handle,
537
+ ptr(encodeString(query)),
538
+ grepModeToU8(mode),
539
+ BigInt(maxFileSize),
540
+ maxMatchesPerFile,
541
+ smartCase,
542
+ fileOffset,
543
+ pageLimit,
544
+ BigInt(timeBudgetMs),
545
+ beforeContext,
546
+ afterContext,
547
+ classifyDefinitions,
548
+ );
549
+ return parseResult<unknown>(resultPtr);
550
+ }
551
+
552
+ /**
553
+ * Multi-pattern grep - Aho-Corasick multi-needle search.
554
+ */
555
+ export function ffiMultiGrep(
556
+ handle: NativeHandle,
557
+ patternsJoined: string,
558
+ constraints: string,
559
+ maxFileSize: number,
560
+ maxMatchesPerFile: number,
561
+ smartCase: boolean,
562
+ fileOffset: number,
563
+ pageLimit: number,
564
+ timeBudgetMs: number,
565
+ beforeContext: number,
566
+ afterContext: number,
567
+ classifyDefinitions: boolean,
568
+ ): Result<unknown> {
569
+ const library = loadLibrary();
570
+ const resultPtr = library.symbols.fff_multi_grep(
571
+ handle,
572
+ ptr(encodeString(patternsJoined)),
573
+ ptr(encodeString(constraints)),
574
+ BigInt(maxFileSize),
575
+ maxMatchesPerFile,
576
+ smartCase,
577
+ fileOffset,
578
+ pageLimit,
579
+ BigInt(timeBudgetMs),
580
+ beforeContext,
581
+ afterContext,
582
+ classifyDefinitions,
266
583
  );
267
584
  return parseResult<unknown>(resultPtr);
268
585
  }
@@ -301,8 +618,6 @@ export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<
301
618
  const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
302
619
  const result = parseResult<boolean | string>(resultPtr);
303
620
  if (!result.ok) return result;
304
- // JSON.parse("true") returns boolean true, but we also handle
305
- // the string case defensively.
306
621
  return { ok: true, value: result.value === true || result.value === "true" };
307
622
  }
308
623
 
@@ -323,7 +638,6 @@ export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
323
638
  const resultPtr = library.symbols.fff_refresh_git_status(handle);
324
639
  const result = parseResult<number | string>(resultPtr);
325
640
  if (!result.ok) return result;
326
- // JSON.parse("3") returns 3 (number), parseInt handles both
327
641
  return {
328
642
  ok: true,
329
643
  value: typeof result.value === "number" ? result.value : parseInt(result.value, 10),
@@ -381,41 +695,6 @@ export function ffiHealthCheck(
381
695
  return parseResult<unknown>(resultPtr);
382
696
  }
383
697
 
384
- /**
385
- * Detect workspace roots in the indexed directory.
386
- */
387
- export function ffiDetectWorkspaces(handle: NativeHandle): Result<unknown> {
388
- const library = loadLibrary();
389
- const resultPtr = library.symbols.fff_detect_workspaces(handle);
390
- return parseResult<unknown>(resultPtr);
391
- }
392
-
393
- /**
394
- * Live grep - search file contents.
395
- */
396
- export function ffiLiveGrep(
397
- handle: NativeHandle,
398
- query: string,
399
- optsJson: string,
400
- ): Result<unknown> {
401
- const library = loadLibrary();
402
- const resultPtr = library.symbols.fff_live_grep(
403
- handle,
404
- ptr(encodeString(query)),
405
- ptr(encodeString(optsJson)),
406
- );
407
- return parseResult<unknown>(resultPtr);
408
- }
409
-
410
- /**
411
- * Multi-pattern grep - Aho-Corasick multi-needle search.
412
- */
413
- export function ffiMultiGrep(handle: NativeHandle, optsJson: string): Result<unknown> {
414
- const library = loadLibrary();
415
- const resultPtr = library.symbols.fff_multi_grep(handle, ptr(encodeString(optsJson)));
416
- return parseResult<unknown>(resultPtr);
417
- }
418
-
419
698
  /**
420
699
  * Ensure the library is loaded.
421
700
  *
package/src/finder.ts CHANGED
@@ -40,14 +40,7 @@ import type {
40
40
  SearchResult,
41
41
  } from "./types";
42
42
 
43
- import {
44
- createGrepCursor,
45
- err,
46
- toInternalGrepOptions,
47
- toInternalInitOptions,
48
- toInternalMultiGrepOptions,
49
- toInternalSearchOptions,
50
- } from "./types";
43
+ import { createGrepCursor, err } from "./types";
51
44
 
52
45
  /**
53
46
  * FileFinder - Fast file finder with fuzzy search
@@ -108,8 +101,14 @@ export class FileFinder {
108
101
  * ```
109
102
  */
110
103
  static create(options: InitOptions): Result<FileFinder> {
111
- const internalOpts = toInternalInitOptions(options);
112
- const result = ffiCreate(JSON.stringify(internalOpts));
104
+ const result = ffiCreate(
105
+ options.basePath,
106
+ options.frecencyDbPath ?? "",
107
+ options.historyDbPath ?? "",
108
+ options.useUnsafeNoLock ?? false,
109
+ options.warmupMmapCache ?? false,
110
+ options.aiMode ?? false,
111
+ );
113
112
 
114
113
  if (!result.ok) {
115
114
  return result;
@@ -177,14 +176,16 @@ export class FileFinder {
177
176
  const guard = this.ensureAlive();
178
177
  if (!guard.ok) return guard;
179
178
 
180
- const internalOpts = toInternalSearchOptions(options);
181
- const result = ffiSearch(guard.value, query, JSON.stringify(internalOpts));
182
-
183
- if (!result.ok) {
184
- return result;
185
- }
186
-
187
- return result as Result<SearchResult>;
179
+ return ffiSearch(
180
+ guard.value,
181
+ query,
182
+ options?.currentFile ?? "",
183
+ options?.maxThreads ?? 0,
184
+ options?.pageIndex ?? 0,
185
+ options?.pageSize ?? 0,
186
+ options?.comboBoostMultiplier ?? 0,
187
+ options?.minComboCount ?? 0,
188
+ );
188
189
  }
189
190
 
190
191
  /**
@@ -209,26 +210,38 @@ export class FileFinder {
209
210
  * @example
210
211
  * ```typescript
211
212
  * // First page
212
- * const result = finder.liveGrep("TODO", { mode: "plain" });
213
+ * const result = finder.grep("TODO", { mode: "plain" });
213
214
  * if (result.ok) {
214
215
  * for (const match of result.value.items) {
215
216
  * console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
216
217
  * }
217
218
  * // Fetch next page
218
219
  * if (result.value.nextCursor) {
219
- * const page2 = finder.liveGrep("TODO", {
220
+ * const page2 = finder.grep("TODO", {
220
221
  * cursor: result.value.nextCursor,
221
222
  * });
222
223
  * }
223
224
  * }
224
225
  * ```
225
226
  */
226
- liveGrep(query: string, options?: GrepOptions): Result<GrepResult> {
227
+ grep(query: string, options?: GrepOptions): Result<GrepResult> {
227
228
  const guard = this.ensureAlive();
228
229
  if (!guard.ok) return guard;
229
230
 
230
- const internalOpts = toInternalGrepOptions(options);
231
- const result = ffiLiveGrep(guard.value, query, JSON.stringify(internalOpts));
231
+ const result = ffiLiveGrep(
232
+ guard.value,
233
+ query,
234
+ options?.mode ?? "plain",
235
+ options?.maxFileSize ?? 0,
236
+ options?.maxMatchesPerFile ?? 0,
237
+ options?.smartCase ?? true,
238
+ options?.cursor?._offset ?? 0,
239
+ 0, // page_limit (0 = default 50)
240
+ options?.timeBudgetMs ?? 0,
241
+ options?.beforeContext ?? 0,
242
+ options?.afterContext ?? 0,
243
+ false,
244
+ );
232
245
 
233
246
  return transformGrepResult(result);
234
247
  }
@@ -266,8 +279,20 @@ export class FileFinder {
266
279
  return err("patterns array must have at least 1 element");
267
280
  }
268
281
 
269
- const internalOpts = toInternalMultiGrepOptions(options);
270
- const result = ffiMultiGrep(guard.value, JSON.stringify(internalOpts));
282
+ const result = ffiMultiGrep(
283
+ guard.value,
284
+ options.patterns.join("\n"),
285
+ options.constraints ?? "",
286
+ options.maxFileSize ?? 0,
287
+ options.maxMatchesPerFile ?? 0,
288
+ options.smartCase ?? true,
289
+ options.cursor?._offset ?? 0,
290
+ 0, // page_limit (0 = default 50)
291
+ options.timeBudgetMs ?? 0,
292
+ options.beforeContext ?? 0,
293
+ options.afterContext ?? 0,
294
+ false,
295
+ );
271
296
 
272
297
  return transformGrepResult(result);
273
298
  }
@@ -107,7 +107,7 @@ async function waitForFileCount(finder: FileFinder, count: number): Promise<numb
107
107
  return result.ok ? result.value.totalFiles : -1;
108
108
  }
109
109
 
110
- /** Poll liveGrep until predicate on totalMatched is satisfied, or the timeout is exceeded. */
110
+ /** Poll grep until predicate on totalMatched is satisfied, or the timeout is exceeded. */
111
111
  async function waitForGrep(
112
112
  finder: FileFinder,
113
113
  pattern: string,
@@ -116,11 +116,11 @@ async function waitForGrep(
116
116
  ) {
117
117
  const start = Date.now();
118
118
  while (Date.now() - start < WATCHER_TIMEOUT_MS) {
119
- const result = finder.liveGrep(pattern, options);
119
+ const result = finder.grep(pattern, options);
120
120
  if (result.ok && predicate(result.value.totalMatched)) return result;
121
121
  await sleep(POLL_INTERVAL_MS);
122
122
  }
123
- return finder.liveGrep(pattern, options);
123
+ return finder.grep(pattern, options);
124
124
  }
125
125
 
126
126
  describe.skipIf(process.platform === "win32")("Git lifecycle integration", () => {
package/src/index.test.ts CHANGED
@@ -162,8 +162,8 @@ describe("FileFinder - Full Lifecycle", () => {
162
162
  }
163
163
  });
164
164
 
165
- test("liveGrep plain text returns matching lines", () => {
166
- const result = finder.liveGrep("fff-core", {
165
+ test("grep plain text returns matching lines", () => {
166
+ const result = finder.grep("fff-core", {
167
167
  mode: "plain",
168
168
  });
169
169
  expect(result.ok).toBe(true);
@@ -197,9 +197,9 @@ describe("FileFinder - Full Lifecycle", () => {
197
197
  }
198
198
  });
199
199
 
200
- test("liveGrep fuzzy mode returns results with scores", () => {
200
+ test("grep fuzzy mode returns results with scores", () => {
201
201
  // Intentional typo: "depdnency" instead of "dependency" to exercise fuzzy matching
202
- const result = finder.liveGrep("depdnency", {
202
+ const result = finder.grep("depdnency", {
203
203
  mode: "fuzzy",
204
204
  });
205
205
  expect(result.ok).toBe(true);
package/src/types.ts CHANGED
@@ -208,62 +208,6 @@ export interface HealthCheck {
208
208
  };
209
209
  }
210
210
 
211
- /**
212
- * Internal: Options format sent to Rust FFI
213
- * @internal
214
- */
215
- export interface InitOptionsInternal {
216
- base_path: string;
217
- frecency_db_path?: string;
218
- history_db_path?: string;
219
- use_unsafe_no_lock: boolean;
220
- warmup_mmap_cache: boolean;
221
- ai_mode: boolean;
222
- }
223
-
224
- /**
225
- * Internal: Search options format sent to Rust FFI
226
- * @internal
227
- */
228
- export interface SearchOptionsInternal {
229
- max_threads?: number;
230
- current_file?: string;
231
- combo_boost_multiplier?: number;
232
- min_combo_count?: number;
233
- page_index?: number;
234
- page_size?: number;
235
- }
236
-
237
- /**
238
- * Convert public InitOptions to internal format
239
- * @internal
240
- */
241
- export function toInternalInitOptions(opts: InitOptions): InitOptionsInternal {
242
- return {
243
- base_path: opts.basePath,
244
- frecency_db_path: opts.frecencyDbPath,
245
- history_db_path: opts.historyDbPath,
246
- use_unsafe_no_lock: opts.useUnsafeNoLock ?? false,
247
- warmup_mmap_cache: opts.warmupMmapCache ?? false,
248
- ai_mode: opts.aiMode ?? false,
249
- };
250
- }
251
-
252
- /**
253
- * Convert public SearchOptions to internal format
254
- * @internal
255
- */
256
- export function toInternalSearchOptions(opts?: SearchOptions): SearchOptionsInternal {
257
- return {
258
- max_threads: opts?.maxThreads,
259
- current_file: opts?.currentFile,
260
- combo_boost_multiplier: opts?.comboBoostMultiplier,
261
- min_combo_count: opts?.minComboCount,
262
- page_index: opts?.pageIndex,
263
- page_size: opts?.pageSize,
264
- };
265
- }
266
-
267
211
  /**
268
212
  * Grep search mode
269
213
  */
@@ -417,78 +361,3 @@ export interface MultiGrepOptions {
417
361
  afterContext?: number;
418
362
  }
419
363
 
420
- /**
421
- * Internal: Multi-grep options format sent to Rust FFI
422
- * @internal
423
- */
424
- export interface MultiGrepOptionsInternal {
425
- patterns: string[];
426
- constraints?: string;
427
- max_file_size?: number;
428
- max_matches_per_file?: number;
429
- smart_case?: boolean;
430
- file_offset?: number;
431
- page_limit?: number;
432
- time_budget_ms?: number;
433
- before_context?: number;
434
- after_context?: number;
435
- }
436
-
437
- /**
438
- * Convert public MultiGrepOptions to internal format
439
- * @internal
440
- */
441
- export function toInternalMultiGrepOptions(
442
- opts: MultiGrepOptions,
443
- pageLimit?: number,
444
- ): MultiGrepOptionsInternal {
445
- return {
446
- patterns: opts.patterns,
447
- constraints: opts.constraints,
448
- max_file_size: opts.maxFileSize,
449
- max_matches_per_file: opts.maxMatchesPerFile,
450
- smart_case: opts.smartCase,
451
- file_offset: opts.cursor?._offset ?? 0,
452
- page_limit: pageLimit,
453
- time_budget_ms: opts.timeBudgetMs,
454
- before_context: opts.beforeContext,
455
- after_context: opts.afterContext,
456
- };
457
- }
458
-
459
- /**
460
- * Internal: Grep options format sent to Rust FFI
461
- * @internal
462
- */
463
- export interface GrepOptionsInternal {
464
- max_file_size?: number;
465
- max_matches_per_file?: number;
466
- smart_case?: boolean;
467
- file_offset?: number;
468
- page_limit?: number;
469
- mode?: string;
470
- time_budget_ms?: number;
471
- before_context?: number;
472
- after_context?: number;
473
- }
474
-
475
- /**
476
- * Convert public GrepOptions to internal format
477
- * @internal
478
- */
479
- export function toInternalGrepOptions(
480
- opts?: GrepOptions,
481
- pageLimit?: number,
482
- ): GrepOptionsInternal {
483
- return {
484
- max_file_size: opts?.maxFileSize,
485
- max_matches_per_file: opts?.maxMatchesPerFile,
486
- smart_case: opts?.smartCase,
487
- file_offset: opts?.cursor?._offset ?? 0,
488
- page_limit: pageLimit,
489
- mode: opts?.mode,
490
- time_budget_ms: opts?.timeBudgetMs,
491
- before_context: opts?.beforeContext,
492
- after_context: opts?.afterContext,
493
- };
494
- }