Repository navigation
Expand file tree
/
Copy pathfff-api.ts
More file actions
693 lines (639 loc) · 22.6 KB
/
Copy pathfff-api.ts
File metadata and controls
693 lines (639 loc) · 22.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
// ----------------------------------------------------------------------------
// GENERATED FILE - DO NOT EDIT.
// Copied from: packages/shared/fff-api.ts
// Run make sync-js-api from the repo root to regenerate.
// ----------------------------------------------------------------------------
/**
* The shared public API surface for the fff file finder, implemented identically
* by `@ff-labs/fff-node` and `@ff-labs/fff-bun`.
*
* This file is the single source of truth for every type, helper, and the
* `FileFinderApi` interface that crosses the package boundary. It is copied
* verbatim into each package's `src/fff-api.ts` by `make sync-api`.
*
* Anything that is not part of the public API (FFI struct layouts, binary
* loading, platform detection, etc.) stays as per-package internal
* implementation and must NOT live here.
*
* Keep this file self-contained: it must not import from any package-local
* module, since each package compiles its own copy.
*/
/**
* Result type for all operations - follows the Result pattern
*/
export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
/**
* Helper to create a successful result
*/
export function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
/**
* Helper to create an error result
*/
export function err<T>(error: string): Result<T> {
return { ok: false, error };
}
/**
* Initialization options for the file finder
*/
export interface InitOptions {
/** Base directory to index (required) */
basePath: string;
/** Extra `.gitignore` rules relative to `basePath`. Use `!pattern` to re-include paths. Requires a zlob build. */
extraIgnore?: readonly string[];
/** Disable Git integration and Git ignore rules for this index. Requires a zlob build. */
disableGit?: boolean;
/** Skip rules from `.ignore` files. Requires a zlob build. */
disableIgnoreFiles?: boolean;
/** Path to frecency database (optional, omit to skip frecency initialization) */
frecencyDbPath?: string;
/** Path to query history database (optional, omit to skip query tracker initialization) */
historyDbPath?: string;
/** @deprecated no-op */
useUnsafeNoLock?: boolean;
/**
* Disable mmap cache warmup after the initial scan. When mmap cache is
* enabled (the default), the first grep search is as fast as subsequent
* ones at the cost of a longer scan time and higher initial memory usage.
*/
disableMmapCache?: boolean;
/**
* Disable the content index built after the initial scan.
* Content indexing enables faster content-aware filtering during grep.
*/
disableContentIndexing?: boolean;
/**
* Disable the background file-system watcher. When the watcher is
* disabled, files are scanned once but not monitored for changes.
* (default: false)
*/
disableWatch?: boolean;
/** enables optimizations for AI agent assistants. Provide as true if running via mcp/agent */
aiMode?: boolean;
/**
* Path to the tracing log file. When set, the shared FFF tracing subscriber
* is installed on first init and file output is written here. Omit to leave
* logging uninitialized.
*/
logFilePath?: string;
/**
* Log level for the tracing subscriber: "trace", "debug", "info", "warn",
* or "error". Defaults to "info". Ignored when `logFilePath` is not set.
*/
logLevel?: "trace" | "debug" | "info" | "warn" | "error";
/**
* Override for the content cache file-count cap. When omitted, the picker
* auto-sizes the budget from the final scanned file count.
*/
cacheBudgetMaxFiles?: number;
/** Override for the content cache byte cap. See `cacheBudgetMaxFiles`. */
cacheBudgetMaxBytes?: number;
/** Override for the per-file byte cap in the content cache. */
cacheBudgetMaxFileSize?: number;
/**
* Allow indexing the filesystem root (`/`).
* Off by default, having fff instance at the large folder will generally require
* file watcher and indexing which will consume a lot of resources if performed uncontrolled
**/
enableFsRootScanning?: boolean;
/** Allow indexing the user's home directory. Same trade-off as `enableFsRootScanning`. */
enableHomeDirScanning?: boolean;
/** Follow symlinks for directories */
followSymlinks?: boolean;
}
/**
* Search options for fuzzy file search
*/
export interface SearchOptions {
/** Maximum threads for parallel search (0 = auto) */
maxThreads?: number;
/** Current file path (for deprioritization in results) */
currentFile?: string;
/** Combo boost score multiplier (default: 100) */
comboBoostMultiplier?: number;
/** Minimum combo count for boost (default: 3) */
minComboCount?: number;
/** Page index for pagination (default: 0) */
pageIndex?: number;
/** Page size for pagination (default: 100) */
pageSize?: number;
}
/**
* Options for `glob`, the constraint-only search.
*
* The pattern is applied as a single pass SIMD optimized prefiltering
* without any fuzzy matching involved. Faster and 100% compatible to npm `glob`.
*/
export interface GlobOptions {
/** Maximum threads for parallel filtering (0 = auto). */
maxThreads?: number;
/** Current file path (for deprioritization in results). */
currentFile?: string;
/** Page index for pagination (default: 0). */
pageIndex?: number;
/** Page size for pagination (default: 100). */
pageSize?: number;
}
/**
* A file item in search results
*/
export interface FileItem {
/** Path relative to the indexed directory */
relativePath: string;
/** File name only */
fileName: string;
/** File size in bytes */
size: number;
/** Last modified timestamp (Unix seconds) */
modified: number;
/** Frecency score based on access patterns */
accessFrecencyScore: number;
/** Frecency score based on modification time */
modificationFrecencyScore: number;
/** Combined frecency score */
totalFrecencyScore: number;
/** Git status: 'clean', 'modified', 'untracked', 'staged_new', etc. */
gitStatus: string;
}
/**
* Score breakdown for a search result
*/
export interface Score {
/** Total combined score */
total: number;
/** Base fuzzy match score */
baseScore: number;
/** Bonus for filename match */
filenameBonus: number;
/** Bonus for special filenames (index.ts, main.rs, etc.) */
specialFilenameBonus: number;
/** Boost from frecency */
frecencyBoost: number;
/** Penalty for distance in path */
distancePenalty: number;
/** Penalty if this is the current file */
currentFilePenalty: number;
/** Boost from query history combo matching */
comboMatchBoost: number;
/** Bonus for query segments aligning with path segments */
pathAlignmentBonus: number;
/** Whether this was an exact match */
exactMatch: boolean;
/** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
matchType: string;
}
/**
* Location in file (from query like "file.ts:42")
*/
export type Location =
| { type: "line"; line: number }
| { type: "position"; line: number; col: number }
| {
type: "range";
start: { line: number; col: number };
end: { line: number; col: number };
};
/**
* Search result from fuzzy file search
*/
export interface SearchResult {
/** Matched file items */
items: FileItem[];
/** Corresponding scores for each item */
scores: Score[];
/** Total number of files that matched */
totalMatched: number;
/** Total number of indexed files */
totalFiles: number;
/** Location parsed from query (e.g., "file.ts:42:10") */
location?: Location;
}
/**
* A directory item in search results
*/
export interface DirItem {
/** Path relative to the indexed directory (e.g., "src/components/") */
relativePath: string;
/** Last path segment (e.g., "components/" for "src/components/") */
dirName: string;
/** Maximum access frecency score among direct child files */
maxAccessFrecency: number;
}
/**
* Search options for directory search (subset of SearchOptions)
*/
export interface DirSearchOptions {
/** Maximum threads for parallel search (0 = auto) */
maxThreads?: number;
/** Current file path (for distance scoring) */
currentFile?: string;
/** Page index for pagination (default: 0) */
pageIndex?: number;
/** Page size for pagination (default: 100) */
pageSize?: number;
}
/**
* Search result from fuzzy directory search
*/
export interface DirSearchResult {
/** Matched directory items */
items: DirItem[];
/** Corresponding scores for each item */
scores: Score[];
/** Total number of directories that matched */
totalMatched: number;
/** Total number of indexed directories */
totalDirs: number;
}
/**
* A single item in a mixed (files + directories) search result
*/
export type MixedItem =
| { type: "file"; item: FileItem }
| { type: "directory"; item: DirItem };
/**
* Search result from mixed (files + directories) fuzzy search.
* Items are interleaved by total score in descending order.
*/
export interface MixedSearchResult {
/** Matched items (files and directories interleaved by score) */
items: MixedItem[];
/** Corresponding scores for each item */
scores: Score[];
/** Total number of items (files + dirs) that matched */
totalMatched: number;
/** Total number of indexed files */
totalFiles: number;
/** Total number of indexed directories */
totalDirs: number;
/** Location parsed from query */
location?: Location;
}
/**
* Scan progress information
*/
export interface ScanProgress {
/** Number of files scanned so far */
scannedFilesCount: number;
/** Whether a scan is currently in progress */
isScanning: boolean;
/** Whether the background file watcher is ready */
isWatcherReady: boolean;
/** Whether the warmup/bigram phase has completed */
isWarmupComplete: boolean;
}
/**
* Normalized watch event kind.
* A file removed and recreated in one processed batch is marked as modified.
*
* rescan = internal OS buffers were overloaded, some events might be missing.
* The `path` is going to be a folder needs to be rescanned
*
* renamed = the file was moved to the other path without changing the content
*/
export type WatchEventKind = "created" | "modified" | "removed" | "rescan" | "renamed";
/** A single filesystem change notification. */
export interface WatchEvent {
/** Absolute path of the affected file (base path to rescan if `kind == rescan`) */
path: string;
kind: WatchEventKind;
/** Absolute path the file moved from. Only set when `kind == "renamed"`. */
from?: string;
}
/** Options for watch subscriptions. */
export interface WatchOptions {
/** Additional glob wildcard patterns to ignore */
ignore?: string[];
}
/**
* Receives normalized batches of up to 128 events. Each path appears once.
*/
export type WatchBatchCallback = (events: WatchEvent[]) => void;
/** Call me to unsubscribe. */
export type WatchUnsubscribe = () => void;
/** Database health information */
export interface DbHealth {
/** Path to the database */
path: string;
/** Size of the database on disk in bytes */
diskSize: number;
}
/**
* Health check result
*/
export interface HealthCheck {
/** Library version */
version: string;
/** Git integration status */
git: {
/** Whether git2 library is available */
available: boolean;
/** Whether a git repository was found */
repositoryFound: boolean;
/** Git working directory path */
workdir?: string;
/** libgit2 version string */
libgit2Version: string;
/** Error message if git detection failed */
error?: string;
};
/** File picker status */
filePicker: {
/** Whether the file picker is initialized */
initialized: boolean;
/** Base path being indexed */
basePath?: string;
/** Whether a scan is in progress */
isScanning?: boolean;
/** Number of indexed files */
indexedFiles?: number;
/** Error message if there's an issue */
error?: string;
};
/** Frecency database status */
frecency: {
/** Whether frecency tracking is initialized */
initialized: boolean;
/** Database health information */
dbHealthcheck?: DbHealth;
/** Error message if there's an issue */
error?: string;
};
/** Query tracker status */
queryTracker: {
/** Whether query tracking is initialized */
initialized: boolean;
/** Database health information */
dbHealthcheck?: DbHealth;
/** Error message if there's an issue */
error?: string;
};
}
/**
* Grep search mode
*/
export type GrepMode = "plain" | "regex" | "fuzzy";
export type DefinitionMode = "off" | "classify" | "definition_only" | "usage_only";
/**
* Opaque pagination cursor for grep results.
* Pass this to `GrepOptions.cursor` to fetch the next page.
* Do not construct or modify this — use the `nextCursor` from a previous `GrepResult`.
*/
export interface GrepCursor {
/** @internal */
readonly __brand: "GrepCursor";
/** @internal */
readonly _offset: number;
}
/**
* @internal Create a GrepCursor from a raw file offset.
*/
export function createGrepCursor(offset: number): GrepCursor {
return { __brand: "GrepCursor" as const, _offset: offset };
}
/**
* Options for live grep (content search)
*
* Files are searched sequentially in frecency order (most recently/frequently
* accessed first). The engine returns a `nextCursor` for fetching the next page.
*/
export interface GrepOptions {
/** Maximum file size to search in bytes. Files larger than this are skipped. (default: 10MB) */
maxFileSize?: number;
/** Maximum matching lines to collect from a single file (default: 200) */
maxMatchesPerFile?: number;
/** Smart case: case-insensitive when the query is all lowercase, case-sensitive otherwise (default: true) */
smartCase?: boolean;
/**
* Pagination cursor from a previous `GrepResult.nextCursor`.
* Omit (or pass `null`) for the first page.
*/
cursor?: GrepCursor | null;
/** Search mode (default: "plain") */
mode?: GrepMode;
/**
* Maximum wall-clock time in milliseconds to spend searching before returning
* partial results. 0 = unlimited. (default: 0)
*/
timeBudgetMs?: number;
/**
* Apply `timeBudgetMs` even before anything matched. Off by default, so a
* zero-match query scans every candidate file. (default: false)
*/
enforceTimeBudget?: boolean;
/** Number of context lines to include before each match (default: 0) */
beforeContext?: number;
/** Number of context lines to include after each match (default: 0) */
afterContext?: number;
/** Maximum matches to return in this page across all files (default: 50) */
pageSize?: number;
/** Select matching definitions/usages, or classify all matches. Defaults to "off".
* Legacy booleans map false to "off" and true to "classify". Keep the same mode with a cursor. */
classifyDefinitions?: DefinitionMode | boolean;
}
/**
* A single grep match with file and line information
*/
export interface GrepMatch {
/** Path relative to the indexed directory */
relativePath: string;
/** File name only */
fileName: string;
/** Git status */
gitStatus: string;
/** File size in bytes */
size: number;
/** Last modified timestamp (Unix seconds) */
modified: number;
/** Whether the file is binary */
isBinary: boolean;
/** Combined frecency score */
totalFrecencyScore: number;
/** Access-based frecency score */
accessFrecencyScore: number;
/** Modification-based frecency score */
modificationFrecencyScore: number;
/** 1-based line number of the match */
lineNumber: number;
/** 0-based byte column of first match start */
col: number;
/** Absolute byte offset of the matched line from file start */
byteOffset: number;
/** The matched line text (may be truncated) */
lineContent: string;
/** Byte offset pairs [start, end] within lineContent for highlighting */
matchRanges: [number, number][];
/** Fuzzy match score (only in fuzzy mode) */
fuzzyScore?: number;
/** Lines before the match (context). Empty array when context is 0. */
contextBefore?: string[];
/** Lines after the match (context). Empty array when context is 0. */
contextAfter?: string[];
/** Whether the query matches a declared name. Available when classification is enabled. */
isDefinition?: boolean;
}
/**
* Result from a grep search
*/
export interface GrepResult {
/** Matched items with file and line information. At most `max_matches_per_file`. */
items: GrepMatch[];
/** Total number of matches collected (always equal to items.length). */
totalMatched: number;
/** Number of files actually opened and searched in this call */
totalFilesSearched: number;
/** Total number of indexed files (before any filtering) */
totalFiles: number;
/** Number of files eligible for search after filtering out binary files, oversized files, and constraint mismatches */
filteredFileCount: number;
/**
* Cursor for the next page, or `null` if all eligible files have been searched.
* Pass this as `GrepOptions.cursor` to continue from where this call left off.
*/
nextCursor: GrepCursor | null;
/** When regex mode fails to compile the pattern, the engine falls back to literal matching and this field contains the compilation error */
regexFallbackError?: string;
}
/**
* Options for multi-pattern grep (Aho-Corasick multi-needle search)
*
* Searches for lines matching ANY of the provided patterns using
* SIMD-accelerated Aho-Corasick multi-pattern matching.
*/
export interface MultiGrepOptions {
/** Patterns to search for (OR logic — matches lines containing any pattern) */
patterns: string[];
/** File constraints like "*.rs" or "/src/" */
constraints?: string;
/** Maximum file size to search in bytes (default: 10MB) */
maxFileSize?: number;
/** Maximum matching lines to collect from a single file (default: 0 = unlimited) */
maxMatchesPerFile?: number;
/** Smart case: case-insensitive when all patterns are lowercase (default: true) */
smartCase?: boolean;
/**
* Pagination cursor from a previous `GrepResult.nextCursor`.
* Omit (or pass `null`) for the first page.
*/
cursor?: GrepCursor | null;
/**
* Maximum wall-clock time in milliseconds to spend searching before returning
* partial results. 0 = unlimited. (default: 0)
*/
timeBudgetMs?: number;
/**
* Apply `timeBudgetMs` even before anything matched. Off by default, so a
* zero-match query scans every candidate file. (default: false)
*/
enforceTimeBudget?: boolean;
/** Number of context lines to include before each match (default: 0) */
beforeContext?: number;
/** Number of context lines to include after each match (default: 0) */
afterContext?: number;
/** Maximum matches to return in this page across all files (default: 50) */
pageSize?: number;
/** Select matching definitions/usages, or classify all matches. Defaults to "off".
* Legacy booleans map false to "off" and true to "classify". Keep the same mode with a cursor. */
classifyDefinitions?: DefinitionMode | boolean;
}
/**
* The shared instance surface implemented by `FileFinder` in both
* `@ff-labs/fff-node` and `@ff-labs/fff-bun`.
*
* Both packages must implement this identically. Only instance members belong
* here. Static helpers (`create`, `isAvailable`, `ensureLoaded`,
* `healthCheckStatic`) are package-specific and intentionally excluded.
*/
export interface FileFinderApi {
/** Whether the instance has been destroyed. */
readonly isDestroyed: boolean;
/** Destroy and free all native resources. */
destroy(): void;
/** Fuzzy file search. */
fileSearch(query: string, options?: SearchOptions): Result<SearchResult>;
/** Glob-only filtering (no fuzzy matching). */
glob(pattern: string, options?: GlobOptions): Result<SearchResult>;
/** Fuzzy directory search. */
directorySearch(query: string, options?: DirSearchOptions): Result<DirSearchResult>;
/** Fuzzy search over files and directories interleaved by score. */
mixedSearch(query: string, options?: SearchOptions): Result<MixedSearchResult>;
/** Content search (live grep). */
grep(query: string, options?: GrepOptions): Result<GrepResult>;
/** Multi-pattern OR content search (Aho-Corasick). */
multiGrep(options: MultiGrepOptions): Result<GrepResult>;
/** Trigger an async rescan of the indexed directory. */
scanFiles(): Result<void>;
/** Whether a scan is currently in progress. */
isScanning(): boolean;
/** The root directory being indexed. */
getBasePath(): Result<string | null>;
/** Current scan progress snapshot. */
getScanProgress(): Result<ScanProgress>;
/**
* Wait for the initial file scan to complete.
*
* Non-blocking: polls `isScanning` and yields to the event loop between
* checks, so other async work keeps running while waiting.
*/
waitForScan(timeoutMs?: number): Promise<Result<boolean>>;
/**
* Wait for the initial file scan to complete, blocking the calling thread.
*
* Backed by the native `fff_wait_for_scan` call. Prefer `waitForScan` unless
* you specifically need synchronous blocking behaviour.
*/
waitForScanBlocking(timeoutMs?: number): Result<boolean>;
/**
* Wait until the index is fully ready: the scan has finished and the warmup
* (content indexing / bigram) phase has completed.
*
* Non-blocking: polls `getScanProgress` and yields to the event loop.
*/
waitForIndexReady(timeoutMs?: number): Promise<Result<boolean>>;
/** Restart indexing in a new directory. */
reindex(newPath: string): Result<void>;
/** Refresh the git status cache. Returns the number of updated files. */
refreshGitStatus(): Result<number>;
/** Record that `selectedFilePath` was chosen for `query`. */
trackQuery(query: string, selectedFilePath: string): Result<boolean>;
/** Get a historical query by offset (0 = most recent). */
getHistoricalQuery(offset: number): Result<string | null>;
/**
* Subscribe to filesystem changes matching `pattern`.
*
* Patterns may be base-relative globs (./ works), exact paths inside the indexed
* tree, or existing directories. An empty pattern watches the whole tree.
*
* Events are debounced over a 50-ms window and submitted in batches of at most 128 events.
* Gitignored and other ignored files are never triggering watcher.
*/
watch(callback: WatchBatchCallback, options?: WatchOptions): Result<WatchUnsubscribe>;
watch(
pattern: string,
callback: WatchBatchCallback,
options?: WatchOptions,
): Result<WatchUnsubscribe>;
/** Health/diagnostics information for this instance. */
healthCheck(testPath?: string): Result<HealthCheck>;
}
/** @internal Convert the public mode to the C integer argument. */
export function definitionModeToInt(
mode: DefinitionMode | boolean | undefined,
): number | undefined {
switch (mode) {
case undefined:
case false:
case "off":
return 0;
case true:
case "classify":
return 1;
case "definition_only":
return 2;
case "usage_only":
return 3;
default:
return undefined;
}
}