@ff-labs/fff-bun 0.2.4-nightly.d1d5d86 → 0.2.5-dev.14642b8

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.2.4-nightly.d1d5d86",
3
+ "version": "0.2.5-dev.14642b8",
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.2.4-nightly.d1d5d86",
66
- "@ff-labs/fff-bin-darwin-x64": "0.2.4-nightly.d1d5d86",
67
- "@ff-labs/fff-bin-linux-x64-gnu": "0.2.4-nightly.d1d5d86",
68
- "@ff-labs/fff-bin-linux-arm64-gnu": "0.2.4-nightly.d1d5d86",
69
- "@ff-labs/fff-bin-linux-x64-musl": "0.2.4-nightly.d1d5d86",
70
- "@ff-labs/fff-bin-linux-arm64-musl": "0.2.4-nightly.d1d5d86",
71
- "@ff-labs/fff-bin-win32-x64": "0.2.4-nightly.d1d5d86",
72
- "@ff-labs/fff-bin-win32-arm64": "0.2.4-nightly.d1d5d86"
65
+ "@ff-labs/fff-bin-darwin-arm64": "0.2.5-dev.14642b8",
66
+ "@ff-labs/fff-bin-darwin-x64": "0.2.5-dev.14642b8",
67
+ "@ff-labs/fff-bin-linux-x64-gnu": "0.2.5-dev.14642b8",
68
+ "@ff-labs/fff-bin-linux-arm64-gnu": "0.2.5-dev.14642b8",
69
+ "@ff-labs/fff-bin-linux-x64-musl": "0.2.5-dev.14642b8",
70
+ "@ff-labs/fff-bin-linux-arm64-musl": "0.2.5-dev.14642b8",
71
+ "@ff-labs/fff-bin-win32-x64": "0.2.5-dev.14642b8",
72
+ "@ff-labs/fff-bin-win32-arm64": "0.2.5-dev.14642b8"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/bun": "^1.3.8",
package/src/ffi.ts CHANGED
@@ -10,14 +10,44 @@
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";
14
- import { err } from "./types";
13
+ import type {
14
+ FileItem,
15
+ GrepMatch,
16
+ GrepResult,
17
+ Location,
18
+ Result,
19
+ Score,
20
+ SearchResult,
21
+ } from "./types";
22
+ import { createGrepCursor, err } from "./types";
23
+
24
+ /** Grep mode constants matching the C API (u8). */
25
+ const GREP_MODE_PLAIN = 0;
26
+ const GREP_MODE_REGEX = 1;
27
+ const GREP_MODE_FUZZY = 2;
28
+
29
+ /** Map string mode to u8 */
30
+ function grepModeToU8(mode?: string): number {
31
+ switch (mode) {
32
+ case "regex":
33
+ return GREP_MODE_REGEX;
34
+ case "fuzzy":
35
+ return GREP_MODE_FUZZY;
36
+ default:
37
+ return GREP_MODE_PLAIN;
38
+ }
39
+ }
15
40
 
16
- // Define the FFI symbols
17
41
  const ffiDefinition = {
18
- // Lifecycle
19
- fff_create: {
20
- args: [FFIType.cstring],
42
+ fff_create_instance: {
43
+ args: [
44
+ FFIType.cstring, // base_path
45
+ FFIType.cstring, // frecency_db_path
46
+ FFIType.cstring, // history_db_path
47
+ FFIType.bool, // use_unsafe_no_lock
48
+ FFIType.bool, // warmup_mmap_cache
49
+ FFIType.bool, // ai_mode
50
+ ],
21
51
  returns: FFIType.ptr,
22
52
  },
23
53
  fff_destroy: {
@@ -27,19 +57,54 @@ const ffiDefinition = {
27
57
 
28
58
  // Search
29
59
  fff_search: {
30
- args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
60
+ args: [
61
+ FFIType.ptr, // handle
62
+ FFIType.cstring, // query
63
+ FFIType.cstring, // current_file
64
+ FFIType.u32, // max_threads
65
+ FFIType.u32, // page_index
66
+ FFIType.u32, // page_size
67
+ FFIType.i32, // combo_boost_multiplier
68
+ FFIType.u32, // min_combo_count
69
+ ],
31
70
  returns: FFIType.ptr,
32
71
  },
33
72
 
34
73
  // Live grep (content search)
35
74
  fff_live_grep: {
36
- args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
75
+ args: [
76
+ FFIType.ptr, // handle
77
+ FFIType.cstring, // query
78
+ FFIType.u8, // mode
79
+ FFIType.u64, // max_file_size
80
+ FFIType.u32, // max_matches_per_file
81
+ FFIType.bool, // smart_case
82
+ FFIType.u32, // file_offset
83
+ FFIType.u32, // page_limit
84
+ FFIType.u64, // time_budget_ms
85
+ FFIType.u32, // before_context
86
+ FFIType.u32, // after_context
87
+ FFIType.bool, // classify_definitions
88
+ ],
37
89
  returns: FFIType.ptr,
38
90
  },
39
91
 
40
92
  // Multi-pattern grep (Aho-Corasick)
41
93
  fff_multi_grep: {
42
- args: [FFIType.ptr, FFIType.cstring],
94
+ args: [
95
+ FFIType.ptr, // handle
96
+ FFIType.cstring, // patterns_joined (\n-separated)
97
+ FFIType.cstring, // constraints
98
+ FFIType.u64, // max_file_size
99
+ FFIType.u32, // max_matches_per_file
100
+ FFIType.bool, // smart_case
101
+ FFIType.u32, // file_offset
102
+ FFIType.u32, // page_limit
103
+ FFIType.u64, // time_budget_ms
104
+ FFIType.u32, // before_context
105
+ FFIType.u32, // after_context
106
+ FFIType.bool, // classify_definitions
107
+ ],
43
108
  returns: FFIType.ptr,
44
109
  },
45
110
 
@@ -87,6 +152,30 @@ const ffiDefinition = {
87
152
  returns: FFIType.ptr,
88
153
  },
89
154
 
155
+ // Search result accessors / free
156
+ fff_free_search_result: {
157
+ args: [FFIType.ptr],
158
+ returns: FFIType.void,
159
+ },
160
+ fff_search_result_get_item: {
161
+ args: [FFIType.ptr, FFIType.u32],
162
+ returns: FFIType.ptr,
163
+ },
164
+ fff_search_result_get_score: {
165
+ args: [FFIType.ptr, FFIType.u32],
166
+ returns: FFIType.ptr,
167
+ },
168
+
169
+ // Grep result accessors / free
170
+ fff_free_grep_result: {
171
+ args: [FFIType.ptr],
172
+ returns: FFIType.void,
173
+ },
174
+ fff_grep_result_get_match: {
175
+ args: [FFIType.ptr, FFIType.u32],
176
+ returns: FFIType.ptr,
177
+ },
178
+
90
179
  // Memory management
91
180
  fff_free_result: {
92
181
  args: [FFIType.ptr],
@@ -96,6 +185,10 @@ const ffiDefinition = {
96
185
  args: [FFIType.ptr],
97
186
  returns: FFIType.void,
98
187
  },
188
+ fff_free_scan_progress: {
189
+ args: [FFIType.ptr],
190
+ returns: FFIType.void,
191
+ },
99
192
  } as const;
100
193
 
101
194
  type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
@@ -148,59 +241,100 @@ function snakeToCamel(obj: unknown): unknown {
148
241
 
149
242
  const result: Record<string, unknown> = {};
150
243
  for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
151
- const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
244
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
245
+ letter.toUpperCase(),
246
+ );
152
247
  result[camelKey] = snakeToCamel(value);
153
248
  }
154
249
  return result;
155
250
  }
156
251
 
252
+ // ---------------------------------------------------------------------------
253
+ // FffResult byte offsets (must match #[repr(C)] layout on 64-bit)
254
+ // { success: bool(1+7pad), error: *char(8), handle: *void(8), int_value: i64(8) }
255
+ // ---------------------------------------------------------------------------
256
+ const RES_SUCCESS = 0; // bool (1 + 7 padding)
257
+ const RES_ERROR = 8; // *mut c_char (8)
258
+ const RES_HANDLE = 16; // *mut c_void (8)
259
+ const RES_INT_VALUE = 24; // i64 (8)
260
+
157
261
  /**
158
- * Parse a FffResult from the FFI return value.
159
- *
160
- * The result is a pointer to a struct:
161
- * { success: bool, data: *char, error: *char, handle: *void }
162
- *
163
- * Layout (with alignment padding):
164
- * offset 0: success (bool, 1 byte + 7 padding)
165
- * offset 8: data pointer (8 bytes)
166
- * offset 16: error pointer (8 bytes)
167
- * offset 24: handle pointer (8 bytes)
262
+ * Read the FffResult envelope: check success, extract payload, free envelope.
263
+ * On error returns a Result<never>. On success returns the raw handle pointer and int_value.
168
264
  */
169
- function parseResult<T>(resultPtr: Pointer | null): Result<T> {
265
+ function readResultEnvelope(resultPtr: Pointer | null): { success: true; handlePtr: number; intValue: number } | Result<never> {
170
266
  if (resultPtr === null) {
171
267
  return err("FFI returned null pointer");
172
268
  }
173
269
 
174
- const success = read.u8(resultPtr, 0) !== 0;
175
- const dataPtr = read.ptr(resultPtr, 8);
176
- const errorPtr = read.ptr(resultPtr, 16);
177
-
270
+ const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
178
271
  const library = loadLibrary();
179
272
 
180
- if (success) {
181
- const data = readCString(dataPtr);
182
- // Free the result
183
- library.symbols.fff_free_result(resultPtr);
184
-
185
- if (data === null || data === "") {
186
- return { ok: true, value: undefined as T };
187
- }
188
-
189
- try {
190
- const parsed = JSON.parse(data);
191
- // Convert snake_case to camelCase for TypeScript consumers
192
- const transformed = snakeToCamel(parsed) as T;
193
- return { ok: true, value: transformed };
194
- } catch {
195
- // For simple values like "true" or numbers
196
- return { ok: true, value: data as T };
197
- }
198
- } else {
273
+ if (!success) {
274
+ const errorPtr = read.ptr(resultPtr, RES_ERROR);
199
275
  const errorMsg = readCString(errorPtr) || "Unknown error";
200
- // Free the result
201
276
  library.symbols.fff_free_result(resultPtr);
202
277
  return err(errorMsg);
203
278
  }
279
+
280
+ const handlePtr = read.ptr(resultPtr, RES_HANDLE);
281
+ const intValue = Number(read.i64(resultPtr, RES_INT_VALUE));
282
+ library.symbols.fff_free_result(resultPtr);
283
+ return { success: true, handlePtr, intValue };
284
+ }
285
+
286
+ /** Parse a FffResult that carries a bool in int_value (0 = false, nonzero = true). */
287
+ function parseBoolResult(resultPtr: Pointer | null): Result<boolean> {
288
+ const envelope = readResultEnvelope(resultPtr);
289
+ if (!("success" in envelope)) return envelope;
290
+ return { ok: true, value: envelope.intValue !== 0 };
291
+ }
292
+
293
+ /** Parse a FffResult that carries an integer in int_value. */
294
+ function parseIntResult(resultPtr: Pointer | null): Result<number> {
295
+ const envelope = readResultEnvelope(resultPtr);
296
+ if (!("success" in envelope)) return envelope;
297
+ return { ok: true, value: envelope.intValue };
298
+ }
299
+
300
+ /** Parse a FffResult that carries a string in handle (freed with fff_free_string). */
301
+ function parseStringResult(resultPtr: Pointer | null): Result<string | null> {
302
+ const envelope = readResultEnvelope(resultPtr);
303
+ if (!("success" in envelope)) return envelope;
304
+
305
+ if (envelope.handlePtr === 0) return { ok: true, value: null };
306
+
307
+ const library = loadLibrary();
308
+ const str = readCString(envelope.handlePtr);
309
+ library.symbols.fff_free_string(asPtr(envelope.handlePtr));
310
+ return { ok: true, value: str };
311
+ }
312
+
313
+ /** Parse a FffResult that carries a JSON string in handle. */
314
+ function parseJsonResult<T>(resultPtr: Pointer | null): Result<T> {
315
+ const envelope = readResultEnvelope(resultPtr);
316
+ if (!("success" in envelope)) return envelope;
317
+
318
+ if (envelope.handlePtr === 0) return { ok: true, value: undefined as T };
319
+
320
+ const library = loadLibrary();
321
+ const jsonStr = readCString(envelope.handlePtr);
322
+ library.symbols.fff_free_string(asPtr(envelope.handlePtr));
323
+
324
+ if (jsonStr === null || jsonStr === "") return { ok: true, value: undefined as T };
325
+
326
+ try {
327
+ return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) as T };
328
+ } catch {
329
+ return { ok: true, value: jsonStr as T };
330
+ }
331
+ }
332
+
333
+ /** Parse a FffResult with no payload (void, success/error only). */
334
+ function parseVoidResult(resultPtr: Pointer | null): Result<void> {
335
+ const envelope = readResultEnvelope(resultPtr);
336
+ if (!("success" in envelope)) return envelope;
337
+ return { ok: true, value: undefined };
204
338
  }
205
339
 
206
340
  /**
@@ -210,28 +344,39 @@ export type NativeHandle = Pointer;
210
344
 
211
345
  /**
212
346
  * 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
347
  */
217
- export function ffiCreate(optsJson: string): Result<NativeHandle> {
348
+ export function ffiCreate(
349
+ basePath: string,
350
+ frecencyDbPath: string,
351
+ historyDbPath: string,
352
+ useUnsafeNoLock: boolean,
353
+ warmupMmapCache: boolean,
354
+ aiMode: boolean,
355
+ ): Result<NativeHandle> {
218
356
  const library = loadLibrary();
219
- const resultPtr = library.symbols.fff_create(ptr(encodeString(optsJson)));
357
+ const resultPtr = library.symbols.fff_create_instance(
358
+ ptr(encodeString(basePath)),
359
+ ptr(encodeString(frecencyDbPath)),
360
+ ptr(encodeString(historyDbPath)),
361
+ useUnsafeNoLock,
362
+ warmupMmapCache,
363
+ aiMode,
364
+ );
220
365
 
221
366
  if (resultPtr === null) {
222
367
  return err("FFI returned null pointer");
223
368
  }
224
369
 
225
- const success = read.u8(resultPtr, 0) !== 0;
226
- const errorPtr = read.ptr(resultPtr, 16);
227
- const handlePtr = read.ptr(resultPtr, 24);
370
+ const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
371
+ const errorPtr = read.ptr(resultPtr, RES_ERROR);
372
+ const handlePtr = read.ptr(resultPtr, RES_HANDLE);
228
373
 
229
374
  if (success) {
230
375
  const handle = handlePtr as unknown as Pointer;
231
376
  library.symbols.fff_free_result(resultPtr);
232
377
 
233
378
  if (!handle || handle === (0 as unknown as Pointer)) {
234
- return err("fff_create returned null handle");
379
+ return err("fff_create_instance returned null handle");
235
380
  }
236
381
 
237
382
  return { ok: true, value: handle };
@@ -250,21 +395,415 @@ export function ffiDestroy(handle: NativeHandle): void {
250
395
  library.symbols.fff_destroy(handle);
251
396
  }
252
397
 
398
+ // ---------------------------------------------------------------------------
399
+ // Struct byte offsets (must match #[repr(C)] layout on 64-bit)
400
+ // ---------------------------------------------------------------------------
401
+
402
+ // FffSearchResult { items: *mut, scores: *mut, count: u32, total_matched: u32, total_files: u32, location: FffLocation }
403
+ const SR_ITEMS = 0; // *mut FffFileItem (8)
404
+ const SR_SCORES = 8; // *mut FffScore (8)
405
+ const SR_COUNT = 16; // u32 (4)
406
+ const SR_MATCHED = 20; // u32 (4)
407
+ const SR_TOTAL = 24; // u32 (4)
408
+ // FffLocation is inlined at offset 28
409
+ const SR_LOC_TAG = 28; // u8 (1 + 3 padding)
410
+ const SR_LOC_LINE = 32; // i32 (4)
411
+ const SR_LOC_COL = 36; // i32 (4)
412
+ const SR_LOC_END_LINE = 40; // i32 (4)
413
+ const SR_LOC_END_COL = 44; // i32 (4)
414
+
415
+ // FffFileItem (80 bytes)
416
+ const FI_PATH = 0; // *mut c_char (8)
417
+ const FI_RELPATH = 8; // *mut c_char (8)
418
+ const FI_FNAME = 16; // *mut c_char (8)
419
+ const FI_GIT = 24; // *mut c_char (8)
420
+ const FI_SIZE = 32; // u64 (8)
421
+ const FI_MODIFIED = 40; // u64 (8)
422
+ const FI_ACCESS = 48; // i64 (8)
423
+ const FI_MODFR = 56; // i64 (8)
424
+ const FI_TOTAL_FR = 64; // i64 (8)
425
+ const _FI_BINARY = 72; // bool (1 + 7 pad)
426
+ const FI_SIZE_OF = 80;
427
+
428
+ // FffScore (48 bytes)
429
+ const SC_TOTAL = 0; // i32 (4)
430
+ const SC_BASE = 4; // i32 (4)
431
+ const SC_FNAME = 8; // i32 (4)
432
+ const SC_SPECIAL = 12; // i32 (4)
433
+ const SC_FREC = 16; // i32 (4)
434
+ const SC_DIST = 20; // i32 (4)
435
+ const SC_CURFILE = 24; // i32 (4)
436
+ const SC_COMBO = 28; // i32 (4)
437
+ const SC_EXACT = 32; // bool (1 + 7 pad)
438
+ const SC_MTYPE = 40; // *mut c_char (8)
439
+ const SC_SIZE_OF = 48;
440
+
441
+ /** Cast a number (raw address from pointer math) to Pointer for read.*. */
442
+ function asPtr(n: number): Pointer {
443
+ return n as unknown as Pointer;
444
+ }
445
+
446
+ /**
447
+ * Read an FffFileItem struct at the given raw address.
448
+ */
449
+ function readFileItemStruct(p: number): FileItem {
450
+ const pp = asPtr(p);
451
+ return {
452
+ path: readCString(read.ptr(pp, FI_PATH)) ?? "",
453
+ relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
454
+ fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
455
+ gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
456
+ size: Number(read.u64(pp, FI_SIZE)),
457
+ modified: Number(read.u64(pp, FI_MODIFIED)),
458
+ accessFrecencyScore: Number(read.i64(pp, FI_ACCESS)),
459
+ modificationFrecencyScore: Number(read.i64(pp, FI_MODFR)),
460
+ totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Read an FffScore struct at the given raw address.
466
+ */
467
+ function readScoreStruct(p: number): Score {
468
+ const pp = asPtr(p);
469
+ return {
470
+ total: read.i32(pp, SC_TOTAL),
471
+ baseScore: read.i32(pp, SC_BASE),
472
+ filenameBonus: read.i32(pp, SC_FNAME),
473
+ specialFilenameBonus: read.i32(pp, SC_SPECIAL),
474
+ frecencyBoost: read.i32(pp, SC_FREC),
475
+ distancePenalty: read.i32(pp, SC_DIST),
476
+ currentFilePenalty: read.i32(pp, SC_CURFILE),
477
+ comboMatchBoost: read.i32(pp, SC_COMBO),
478
+ exactMatch: read.u8(pp, SC_EXACT) !== 0,
479
+ matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Parse an FffSearchResult from a raw pointer, then free native memory.
485
+ */
486
+ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
487
+ if (resultPtr === null) {
488
+ return err("FFI returned null pointer");
489
+ }
490
+
491
+ const envelope = readResultEnvelope(resultPtr);
492
+ if (!("success" in envelope)) return envelope;
493
+
494
+ if (envelope.handlePtr === 0) {
495
+ return err("fff_search returned null search result");
496
+ }
497
+
498
+ const hp = asPtr(envelope.handlePtr);
499
+ const count = read.u32(hp, SR_COUNT);
500
+ const totalMatched = read.u32(hp, SR_MATCHED);
501
+ const totalFiles = read.u32(hp, SR_TOTAL);
502
+ const itemsBase = read.ptr(hp, SR_ITEMS);
503
+ const scoresBase = read.ptr(hp, SR_SCORES);
504
+
505
+ // Read location
506
+ const locTag = read.u8(hp, SR_LOC_TAG);
507
+ let location: Location | undefined;
508
+ if (locTag === 1) {
509
+ location = { type: "line", line: read.i32(hp, SR_LOC_LINE) };
510
+ } else if (locTag === 2) {
511
+ location = {
512
+ type: "position",
513
+ line: read.i32(hp, SR_LOC_LINE),
514
+ col: read.i32(hp, SR_LOC_COL),
515
+ };
516
+ } else if (locTag === 3) {
517
+ location = {
518
+ type: "range",
519
+ start: { line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) },
520
+ end: {
521
+ line: read.i32(hp, SR_LOC_END_LINE),
522
+ col: read.i32(hp, SR_LOC_END_COL),
523
+ },
524
+ };
525
+ }
526
+
527
+ // Read items and scores arrays using pointer arithmetic
528
+ const items: FileItem[] = [];
529
+ const scores: Score[] = [];
530
+
531
+ for (let i = 0; i < count; i++) {
532
+ items.push(readFileItemStruct(itemsBase + i * FI_SIZE_OF));
533
+ scores.push(readScoreStruct(scoresBase + i * SC_SIZE_OF));
534
+ }
535
+
536
+ // Free native search result
537
+ loadLibrary().symbols.fff_free_search_result(hp);
538
+
539
+ const result: SearchResult = { items, scores, totalMatched, totalFiles };
540
+ if (location) {
541
+ result.location = location;
542
+ }
543
+ return { ok: true, value: result };
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // FffGrepMatch byte offsets (must match #[repr(C)] layout on 64-bit)
548
+ // ---------------------------------------------------------------------------
549
+
550
+ // Pointers (8 bytes each)
551
+ const GM_PATH = 0;
552
+ const GM_RELPATH = 8;
553
+ const GM_FNAME = 16;
554
+ const GM_GIT = 24;
555
+ const GM_LINE_CONTENT = 32;
556
+ const GM_MATCH_RANGES = 40;
557
+ const GM_CTX_BEFORE = 48;
558
+ const GM_CTX_AFTER = 56;
559
+
560
+ // 8-byte numeric fields
561
+ const GM_SIZE = 64;
562
+ const GM_MODIFIED = 72;
563
+ const GM_TOTAL_FR = 80;
564
+ const GM_ACCESS_FR = 88;
565
+ const GM_MOD_FR = 96;
566
+ const GM_LINE_NUM = 104;
567
+ const GM_BYTE_OFF = 112;
568
+
569
+ // 4-byte fields
570
+ const GM_COL = 120;
571
+ const GM_MR_COUNT = 124;
572
+ const GM_CTX_B_COUNT = 128;
573
+ const GM_CTX_A_COUNT = 132;
574
+
575
+ // 2-byte
576
+ const GM_FUZZY_SCORE = 136;
577
+ // 1-byte
578
+ const GM_HAS_FUZZY = 138;
579
+ const GM_IS_BINARY = 139;
580
+ const _GM_IS_DEF = 140;
581
+
582
+ // struct size: pad to 8-byte alignment → 144
583
+ const GM_SIZE_OF = 144;
584
+
585
+ // FffGrepResult
586
+ const GR_ITEMS = 0; // *mut FffGrepMatch (8)
587
+ const GR_COUNT = 8; // u32 (4)
588
+ const GR_MATCHED = 12; // u32 (4)
589
+ const GR_FILES_SEARCHED = 16; // u32 (4)
590
+ const GR_TOTAL_FILES = 20; // u32 (4)
591
+ const GR_FILTERED = 24; // u32 (4)
592
+ const GR_NEXT_OFFSET = 28; // u32 (4)
593
+ const GR_REGEX_ERR = 32; // *mut c_char (8)
594
+
595
+ // FffMatchRange (8 bytes)
596
+ const MR_START = 0;
597
+ const MR_END = 4;
598
+ const MR_SIZE = 8;
599
+
600
+ /**
601
+ * Read a C string array (char**) at the given pointer, with `count` elements.
602
+ */
603
+ function readCStringArray(base: number, count: number): string[] {
604
+ if (count === 0 || base === 0) return [];
605
+ const result: string[] = [];
606
+ const bp = asPtr(base);
607
+ for (let i = 0; i < count; i++) {
608
+ const strPtr = read.ptr(bp, i * 8); // each pointer is 8 bytes
609
+ result.push(readCString(strPtr) ?? "");
610
+ }
611
+ return result;
612
+ }
613
+
614
+ /**
615
+ * Read an FffGrepMatch struct at the given raw address.
616
+ */
617
+ function readGrepMatchStruct(p: number): GrepMatch {
618
+ const pp = asPtr(p);
619
+ const matchRangesPtr = read.ptr(pp, GM_MATCH_RANGES);
620
+ const matchRangesCount = read.u32(pp, GM_MR_COUNT);
621
+ const matchRanges: [number, number][] = [];
622
+ for (let i = 0; i < matchRangesCount; i++) {
623
+ const base = matchRangesPtr + i * MR_SIZE;
624
+ const bp = asPtr(base);
625
+ matchRanges.push([read.u32(bp, MR_START), read.u32(bp, MR_END)]);
626
+ }
627
+
628
+ const hasFuzzy = read.u8(pp, GM_HAS_FUZZY) !== 0;
629
+ const ctxBeforeCount = read.u32(pp, GM_CTX_B_COUNT);
630
+ const ctxAfterCount = read.u32(pp, GM_CTX_A_COUNT);
631
+
632
+ const match: GrepMatch = {
633
+ path: readCString(read.ptr(pp, GM_PATH)) ?? "",
634
+ relativePath: readCString(read.ptr(pp, GM_RELPATH)) ?? "",
635
+ fileName: readCString(read.ptr(pp, GM_FNAME)) ?? "",
636
+ gitStatus: readCString(read.ptr(pp, GM_GIT)) ?? "",
637
+ lineContent: readCString(read.ptr(pp, GM_LINE_CONTENT)) ?? "",
638
+ size: Number(read.u64(pp, GM_SIZE)),
639
+ modified: Number(read.u64(pp, GM_MODIFIED)),
640
+ totalFrecencyScore: Number(read.i64(pp, GM_TOTAL_FR)),
641
+ accessFrecencyScore: Number(read.i64(pp, GM_ACCESS_FR)),
642
+ modificationFrecencyScore: Number(read.i64(pp, GM_MOD_FR)),
643
+ isBinary: read.u8(pp, GM_IS_BINARY) !== 0,
644
+ lineNumber: Number(read.u64(pp, GM_LINE_NUM)),
645
+ col: read.u32(pp, GM_COL),
646
+ byteOffset: Number(read.u64(pp, GM_BYTE_OFF)),
647
+ matchRanges,
648
+ };
649
+
650
+ if (hasFuzzy) {
651
+ match.fuzzyScore = read.u16(pp, GM_FUZZY_SCORE);
652
+ }
653
+ if (ctxBeforeCount > 0) {
654
+ match.contextBefore = readCStringArray(
655
+ read.ptr(pp, GM_CTX_BEFORE),
656
+ ctxBeforeCount,
657
+ );
658
+ }
659
+ if (ctxAfterCount > 0) {
660
+ match.contextAfter = readCStringArray(
661
+ read.ptr(pp, GM_CTX_AFTER),
662
+ ctxAfterCount,
663
+ );
664
+ }
665
+
666
+ return match;
667
+ }
668
+
669
+ /**
670
+ * Parse an FffGrepResult from a raw FffResult pointer, then free native memory.
671
+ */
672
+ function parseGrepResult(resultPtr: Pointer | null): Result<GrepResult> {
673
+ const envelope = readResultEnvelope(resultPtr);
674
+ if (!("success" in envelope)) return envelope;
675
+
676
+ if (envelope.handlePtr === 0) {
677
+ return err("grep returned null result");
678
+ }
679
+
680
+ const hp = asPtr(envelope.handlePtr);
681
+ const count = read.u32(hp, GR_COUNT);
682
+ const totalMatched = read.u32(hp, GR_MATCHED);
683
+ const totalFilesSearched = read.u32(hp, GR_FILES_SEARCHED);
684
+ const totalFiles = read.u32(hp, GR_TOTAL_FILES);
685
+ const filteredFileCount = read.u32(hp, GR_FILTERED);
686
+ const nextFileOffset = read.u32(hp, GR_NEXT_OFFSET);
687
+ const regexErrPtr = read.ptr(hp, GR_REGEX_ERR);
688
+ const regexFallbackError = readCString(regexErrPtr) ?? undefined;
689
+ const itemsBase = read.ptr(hp, GR_ITEMS);
690
+
691
+ const items: GrepMatch[] = [];
692
+ for (let i = 0; i < count; i++) {
693
+ items.push(readGrepMatchStruct(itemsBase + i * GM_SIZE_OF));
694
+ }
695
+
696
+ loadLibrary().symbols.fff_free_grep_result(hp);
697
+
698
+ const grepResult: GrepResult = {
699
+ items,
700
+ totalMatched,
701
+ totalFilesSearched,
702
+ totalFiles,
703
+ filteredFileCount,
704
+ nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
705
+ };
706
+ if (regexFallbackError) {
707
+ grepResult.regexFallbackError = regexFallbackError;
708
+ }
709
+ return { ok: true, value: grepResult };
710
+ }
711
+
253
712
  /**
254
713
  * Perform fuzzy search.
255
714
  */
256
715
  export function ffiSearch(
257
716
  handle: NativeHandle,
258
717
  query: string,
259
- optsJson: string,
260
- ): Result<unknown> {
718
+ currentFile: string,
719
+ maxThreads: number,
720
+ pageIndex: number,
721
+ pageSize: number,
722
+ comboBoostMultiplier: number,
723
+ minComboCount: number,
724
+ ): Result<SearchResult> {
261
725
  const library = loadLibrary();
262
726
  const resultPtr = library.symbols.fff_search(
263
727
  handle,
264
728
  ptr(encodeString(query)),
265
- ptr(encodeString(optsJson)),
729
+ ptr(encodeString(currentFile)),
730
+ maxThreads,
731
+ pageIndex,
732
+ pageSize,
733
+ comboBoostMultiplier,
734
+ minComboCount,
735
+ );
736
+ return parseSearchResult(resultPtr);
737
+ }
738
+
739
+ /**
740
+ * Live grep - search file contents.
741
+ */
742
+ export function ffiLiveGrep(
743
+ handle: NativeHandle,
744
+ query: string,
745
+ mode: string,
746
+ maxFileSize: number,
747
+ maxMatchesPerFile: number,
748
+ smartCase: boolean,
749
+ fileOffset: number,
750
+ pageLimit: number,
751
+ timeBudgetMs: number,
752
+ beforeContext: number,
753
+ afterContext: number,
754
+ classifyDefinitions: boolean,
755
+ ): Result<GrepResult> {
756
+ const library = loadLibrary();
757
+ const resultPtr = library.symbols.fff_live_grep(
758
+ handle,
759
+ ptr(encodeString(query)),
760
+ grepModeToU8(mode),
761
+ BigInt(maxFileSize),
762
+ maxMatchesPerFile,
763
+ smartCase,
764
+ fileOffset,
765
+ pageLimit,
766
+ BigInt(timeBudgetMs),
767
+ beforeContext,
768
+ afterContext,
769
+ classifyDefinitions,
770
+ );
771
+ return parseGrepResult(resultPtr);
772
+ }
773
+
774
+ /**
775
+ * Multi-pattern grep - Aho-Corasick multi-needle search.
776
+ */
777
+ export function ffiMultiGrep(
778
+ handle: NativeHandle,
779
+ patternsJoined: string,
780
+ constraints: string,
781
+ maxFileSize: number,
782
+ maxMatchesPerFile: number,
783
+ smartCase: boolean,
784
+ fileOffset: number,
785
+ pageLimit: number,
786
+ timeBudgetMs: number,
787
+ beforeContext: number,
788
+ afterContext: number,
789
+ classifyDefinitions: boolean,
790
+ ): Result<GrepResult> {
791
+ const library = loadLibrary();
792
+ const resultPtr = library.symbols.fff_multi_grep(
793
+ handle,
794
+ ptr(encodeString(patternsJoined)),
795
+ ptr(encodeString(constraints)),
796
+ BigInt(maxFileSize),
797
+ maxMatchesPerFile,
798
+ smartCase,
799
+ fileOffset,
800
+ pageLimit,
801
+ BigInt(timeBudgetMs),
802
+ beforeContext,
803
+ afterContext,
804
+ classifyDefinitions,
266
805
  );
267
- return parseResult<unknown>(resultPtr);
806
+ return parseGrepResult(resultPtr);
268
807
  }
269
808
 
270
809
  /**
@@ -273,7 +812,7 @@ export function ffiSearch(
273
812
  export function ffiScanFiles(handle: NativeHandle): Result<void> {
274
813
  const library = loadLibrary();
275
814
  const resultPtr = library.symbols.fff_scan_files(handle);
276
- return parseResult<void>(resultPtr);
815
+ return parseVoidResult(resultPtr);
277
816
  }
278
817
 
279
818
  /**
@@ -284,35 +823,54 @@ export function ffiIsScanning(handle: NativeHandle): boolean {
284
823
  return library.symbols.fff_is_scanning(handle) as boolean;
285
824
  }
286
825
 
826
+ // FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1+7pad) }
827
+ const SP_COUNT = 0; // u64 (8)
828
+ const SP_SCANNING = 8; // bool (1 + 7 pad)
829
+
287
830
  /**
288
831
  * Get scan progress.
289
832
  */
290
- export function ffiGetScanProgress(handle: NativeHandle): Result<unknown> {
833
+ export function ffiGetScanProgress(handle: NativeHandle): Result<{ scannedFilesCount: number; isScanning: boolean }> {
291
834
  const library = loadLibrary();
292
835
  const resultPtr = library.symbols.fff_get_scan_progress(handle);
293
- return parseResult<unknown>(resultPtr);
836
+ const envelope = readResultEnvelope(resultPtr);
837
+ if (!("success" in envelope)) return envelope;
838
+
839
+ if (envelope.handlePtr === 0) {
840
+ return err("scan progress returned null");
841
+ }
842
+
843
+ const hp = asPtr(envelope.handlePtr);
844
+ const result = {
845
+ scannedFilesCount: Number(read.u64(hp, SP_COUNT)),
846
+ isScanning: read.u8(hp, SP_SCANNING) !== 0,
847
+ };
848
+ library.symbols.fff_free_scan_progress(hp);
849
+ return { ok: true, value: result };
294
850
  }
295
851
 
296
852
  /**
297
853
  * Wait for scan to complete.
298
854
  */
299
- export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<boolean> {
855
+ export function ffiWaitForScan(
856
+ handle: NativeHandle,
857
+ timeoutMs: number,
858
+ ): Result<boolean> {
300
859
  const library = loadLibrary();
301
860
  const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
302
- const result = parseResult<boolean | string>(resultPtr);
303
- if (!result.ok) return result;
304
- // JSON.parse("true") returns boolean true, but we also handle
305
- // the string case defensively.
306
- return { ok: true, value: result.value === true || result.value === "true" };
861
+ return parseBoolResult(resultPtr);
307
862
  }
308
863
 
309
864
  /**
310
865
  * Restart index in new path.
311
866
  */
312
- export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<void> {
867
+ export function ffiRestartIndex(
868
+ handle: NativeHandle,
869
+ newPath: string,
870
+ ): Result<void> {
313
871
  const library = loadLibrary();
314
872
  const resultPtr = library.symbols.fff_restart_index(handle, ptr(encodeString(newPath)));
315
- return parseResult<void>(resultPtr);
873
+ return parseVoidResult(resultPtr);
316
874
  }
317
875
 
318
876
  /**
@@ -321,13 +879,7 @@ export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<v
321
879
  export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
322
880
  const library = loadLibrary();
323
881
  const resultPtr = library.symbols.fff_refresh_git_status(handle);
324
- const result = parseResult<number | string>(resultPtr);
325
- if (!result.ok) return result;
326
- // JSON.parse("3") returns 3 (number), parseInt handles both
327
- return {
328
- ok: true,
329
- value: typeof result.value === "number" ? result.value : parseInt(result.value, 10),
330
- };
882
+ return parseIntResult(resultPtr);
331
883
  }
332
884
 
333
885
  /**
@@ -344,9 +896,7 @@ export function ffiTrackQuery(
344
896
  ptr(encodeString(query)),
345
897
  ptr(encodeString(filePath)),
346
898
  );
347
- const result = parseResult<boolean | string>(resultPtr);
348
- if (!result.ok) return result;
349
- return { ok: true, value: result.value === true || result.value === "true" };
899
+ return parseBoolResult(resultPtr);
350
900
  }
351
901
 
352
902
  /**
@@ -358,10 +908,7 @@ export function ffiGetHistoricalQuery(
358
908
  ): Result<string | null> {
359
909
  const library = loadLibrary();
360
910
  const resultPtr = library.symbols.fff_get_historical_query(handle, BigInt(offset));
361
- const result = parseResult<string | null>(resultPtr);
362
- if (!result.ok) return result;
363
- if (result.value === null || result.value === "null") return { ok: true, value: null };
364
- return result as Result<string>;
911
+ return parseStringResult(resultPtr);
365
912
  }
366
913
 
367
914
  /**
@@ -378,42 +925,7 @@ export function ffiHealthCheck(
378
925
  handle ?? (0 as unknown as Pointer),
379
926
  ptr(encodeString(testPath)),
380
927
  );
381
- return parseResult<unknown>(resultPtr);
382
- }
383
-
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);
928
+ return parseJsonResult<unknown>(resultPtr);
417
929
  }
418
930
 
419
931
  /**
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 { 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,28 +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));
232
-
233
- return transformGrepResult(result);
231
+ return 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
+ );
234
245
  }
235
246
 
236
247
  /**
@@ -266,10 +277,20 @@ export class FileFinder {
266
277
  return err("patterns array must have at least 1 element");
267
278
  }
268
279
 
269
- const internalOpts = toInternalMultiGrepOptions(options);
270
- const result = ffiMultiGrep(guard.value, JSON.stringify(internalOpts));
271
-
272
- return transformGrepResult(result);
280
+ return ffiMultiGrep(
281
+ guard.value,
282
+ options.patterns.join("\n"),
283
+ options.constraints ?? "",
284
+ options.maxFileSize ?? 0,
285
+ options.maxMatchesPerFile ?? 0,
286
+ options.smartCase ?? true,
287
+ options.cursor?._offset ?? 0,
288
+ 0, // page_limit (0 = default 50)
289
+ options.timeBudgetMs ?? 0,
290
+ options.beforeContext ?? 0,
291
+ options.afterContext ?? 0,
292
+ false,
293
+ );
273
294
  }
274
295
 
275
296
  /**
@@ -410,20 +431,3 @@ export class FileFinder {
410
431
  }
411
432
  }
412
433
 
413
- function transformGrepResult(result: Result<unknown>): Result<GrepResult> {
414
- if (!result.ok) {
415
- return result;
416
- }
417
- const raw = result.value as Record<string, unknown>;
418
- const nextFileOffset = raw.nextFileOffset as number;
419
- const grepResult: GrepResult = {
420
- items: raw.items as GrepResult["items"],
421
- totalMatched: raw.totalMatched as number,
422
- totalFilesSearched: raw.totalFilesSearched as number,
423
- totalFiles: raw.totalFiles as number,
424
- filteredFileCount: raw.filteredFileCount as number,
425
- nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
426
- regexFallbackError: raw.regexFallbackError as string | undefined,
427
- };
428
- return { ok: true, value: grepResult };
429
- }
@@ -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
- }