@ff-labs/pi-fff 0.10.5-dev.774d6bc → 0.10.5-nightly.f565d37

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
@@ -132,16 +132,28 @@ Mode precedence:
132
132
  ## Flags
133
133
 
134
134
  - `--fff-mode <mode>` — set mode (see above)
135
- - `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env)
136
- - `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env)
135
+ - `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env). Optional; see [Data](#data) for the default.
136
+ - `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env). Optional; see [Data](#data) for the default.
137
137
  - `--fff-enable-root-scan` — allow indexing when launched from `/` (also: `FFF_ENABLE_ROOT_SCAN=1` env). FFF refuses to init at the filesystem root by default.
138
138
  - `--fff-enable-home-scan` — index the home directory when launched from `$HOME` (also: `FFF_ENABLE_HOME_SCAN` env). Enabled by default. Disable with `--fff-enable-home-scan=false` or `FFF_ENABLE_HOME_SCAN=0` if your `$HOME` contains huge trees (toolchains, kernel sources, build outputs) that make the background index run for a long time. When launched from `$HOME` with this enabled, pi shows a warning that the whole home tree is being indexed.
139
139
 
140
140
  ## Data
141
141
 
142
- When database paths are provided, FFF stores:
143
- - frecency database file access frequency/recency
144
- - history database query-to-file selection history
142
+ FFF uses two LMDB databases:
143
+ - frecency database - file access frequency/recency, used to rank results
144
+ - history database - query-to-file selection history
145
+
146
+ Each path is resolved independently, in this order:
147
+
148
+ 1. CLI flag — `--fff-frecency-db` / `--fff-history-db`
149
+ 2. Env var — `FFF_FRECENCY_DB` / `FFF_HISTORY_DB`
150
+ 3. An existing [fff.nvim](https://github.com/dmtrKovalenko/fff.nvim) database, so pi reuses the frecency you built up in your editor:
151
+ - frecency: `$XDG_CACHE_HOME/nvim/fff_nvim`
152
+ - history: `$XDG_DATA_HOME/nvim/fff_queries`
153
+ - `XDG_CACHE_HOME` defaults to `~/.cache` and `XDG_DATA_HOME` to `~/.local/share`; on Windows both fall back under `%LOCALAPPDATA%\nvim-data`. Only directories count — a plain file at those paths is ignored.
154
+ 4. pi-local directory, created on demand — `$PI_CODING_AGENT_DIR/fff/{frecency,history}`, defaulting to `~/.pi/agent/fff/{frecency,history}`
155
+
156
+ The extension only reads these databases; it never records the agent's own searches into your Neovim history. If a database cannot be opened, the finder starts without persistence and pi shows a warning instead of failing.
145
157
 
146
158
  No project files are uploaded anywhere by this extension. It runs locally and only uses the configured LLM through pi itself.
147
159
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ff-labs/pi-fff",
3
3
  "public": true,
4
- "version": "0.10.5-dev.774d6bc",
4
+ "version": "0.10.5-nightly.f565d37",
5
5
  "description": "pi extension: FFF-powered fuzzy file and content search",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -40,8 +40,8 @@
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "dependencies": {
43
- "@ff-labs/fff-bun": "0.10.5-dev.774d6bc",
44
- "@ff-labs/fff-node": "0.10.5-dev.774d6bc"
43
+ "@ff-labs/fff-bun": "0.10.5-nightly.f565d37",
44
+ "@ff-labs/fff-node": "0.10.5-nightly.f565d37"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@earendil-works/pi-coding-agent": "*",
@@ -49,6 +49,7 @@
49
49
  "@sinclair/typebox": "*"
50
50
  },
51
51
  "devDependencies": {
52
+ "@types/bun": "^1.3.8",
52
53
  "@types/node": "^22.0.0",
53
54
  "typescript": "^5.0.0"
54
55
  }
@@ -1,8 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { FileFinderApi } from "@ff-labs/fff-node";
4
+ import type { FilePickerFactory } from "./file-picker";
4
5
  import { HOME_DIR } from "./paths";
5
- import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
6
6
 
7
7
  export const MAX_AUX = 3;
8
8
  export const IDLE_TTL_MS = 5 * 60 * 1000;
@@ -16,10 +16,9 @@ interface AuxPicker {
16
16
  export interface AuxOpts {
17
17
  enableFsRootScanning: boolean;
18
18
  enableHomeDirScanning?: boolean;
19
+ pickers: FilePickerFactory;
19
20
  // Called before a newly spawned aux picker starts a scan that covers $HOME.
20
21
  onHomeDirScan?: (root: string) => void;
21
- frecencyDbPath?: string;
22
- historyDbPath?: string;
23
22
  }
24
23
 
25
24
  export class AuxFinderPool {
@@ -101,26 +100,13 @@ export class AuxFinderPool {
101
100
  this.opts.onHomeDirScan?.(root);
102
101
  }
103
102
 
104
- const { FileFinder } = await loadSdk();
105
- const result = FileFinder.create({
103
+ const finder = await this.opts.pickers.create({
106
104
  basePath: root,
107
- frecencyDbPath: this.opts.frecencyDbPath,
108
- historyDbPath: this.opts.historyDbPath,
109
- aiMode: true,
110
105
  enableHomeDirScanning,
111
106
  enableFsRootScanning: this.opts.enableFsRootScanning,
112
107
  });
113
108
 
114
- if (!result.ok) {
115
- throw new Error(`Failed to create aux file finder for ${root}: ${result.error}`);
116
- }
117
-
118
- await result.value.waitForScan(SCAN_TIMEOUT_MS);
119
- const entry: AuxPicker = {
120
- root,
121
- finder: result.value,
122
- lastUsed: Date.now(),
123
- };
109
+ const entry: AuxPicker = { root, finder, lastUsed: Date.now() };
124
110
  this.entries.push(entry);
125
111
  return entry;
126
112
  }
@@ -0,0 +1,73 @@
1
+ import type { FileFinderApi, InitOptions, Result } from "@ff-labs/fff-node";
2
+ import { type FileFinderStatic, loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
3
+
4
+ export interface PickerOptions {
5
+ basePath: string;
6
+ enableHomeDirScanning?: boolean;
7
+ enableFsRootScanning?: boolean;
8
+ }
9
+
10
+ /** Opens every picker in this pi process — the cwd picker and the aux pickers —
11
+ * on the same frecency/history databases. */
12
+ export class FilePickerFactory {
13
+ private dbDisabled = false;
14
+ private readonly frecencyDbPath: string;
15
+ private readonly historyDbPath: string;
16
+ private readonly onDbFailure?: (error: string) => void;
17
+
18
+ constructor(opts: {
19
+ frecencyDbPath: string;
20
+ historyDbPath: string;
21
+ onDbFailure?: (error: string) => void;
22
+ }) {
23
+ this.frecencyDbPath = opts.frecencyDbPath;
24
+ this.historyDbPath = opts.historyDbPath;
25
+ this.onDbFailure = opts.onDbFailure;
26
+ }
27
+
28
+ /** True once the databases were given up on, so pickers open without them. */
29
+ get databasesDisabled(): boolean {
30
+ return this.dbDisabled;
31
+ }
32
+
33
+ /** Opens a scanned, ready-to-use picker. Throws if it cannot be created. */
34
+ async create(options: PickerOptions): Promise<FileFinderApi> {
35
+ const { FileFinder } = await loadSdk();
36
+ const result = this.openWithDbFallback(FileFinder, options);
37
+
38
+ if (!result.ok) {
39
+ throw new Error(
40
+ `Failed to create FFF file picker for ${options.basePath}: ${result.error}`,
41
+ );
42
+ }
43
+
44
+ // waitForScan() also resolves on timeout, so this bounds startup rather
45
+ // than guaranteeing a complete index.
46
+ await result.value.waitForScan(SCAN_TIMEOUT_MS);
47
+ return result.value;
48
+ }
49
+
50
+ private openWithDbFallback(
51
+ FileFinder: FileFinderStatic,
52
+ options: PickerOptions,
53
+ ): Result<FileFinderApi> {
54
+ const init: InitOptions = { ...options, aiMode: true };
55
+ if (this.dbDisabled) return FileFinder.create(init);
56
+
57
+ const result = FileFinder.create({
58
+ ...init,
59
+ frecencyDbPath: this.frecencyDbPath,
60
+ historyDbPath: this.historyDbPath,
61
+ });
62
+ if (result.ok) return result;
63
+
64
+ // A failure here is usually transient (broken lock, corruption) and self-heals
65
+ // on restart, so drop the databases instead of leaving pi without a picker
66
+ const dbLess = FileFinder.create(init);
67
+ if (!dbLess.ok) return result; // db error is the more useful one to report
68
+
69
+ this.dbDisabled = true;
70
+ this.onDbFailure?.(result.error);
71
+ return dbLess;
72
+ }
73
+ }
package/src/index.ts CHANGED
@@ -22,9 +22,9 @@ import type {
22
22
  } from "@ff-labs/fff-node";
23
23
  import { Type } from "@sinclair/typebox";
24
24
  import { AuxFinderPool, routePathConstraint } from "./aux-finders";
25
+ import { FilePickerFactory } from "./file-picker";
26
+ import { isHomeDir, resolveDbPaths } from "./paths";
25
27
  import { buildQuery } from "./query";
26
- import { isHomeDir } from "./paths";
27
- import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
28
28
 
29
29
  export { SCAN_TIMEOUT_MS } from "./sdk";
30
30
 
@@ -162,16 +162,7 @@ export function fffFileAnnotation(item: {
162
162
  return "";
163
163
  }
164
164
 
165
- // fff-core native definition classifier (byte-level scanner in Rust) is enabled
166
- // via GrepOptions.classifyDefinitions. Each GrepMatch carries isDefinition for
167
- // downstream consumers; pi-fff does NOT use it to re-sort.
168
- //
169
- // Ordering policy: NO CUSTOM SORTING. The engine already returns items in
170
- // frecency order (most-accessed files first). pi-fff only groups consecutive
171
- // matches into per-file blocks and preserves whatever order the engine
172
- // provided — inside a file we keep matches in source-line order because the
173
- // engine emits them that way.
174
-
165
+ // DO NOT ATTEMPT TO RESORT OUTPUT HERE IT ONLY CONFUSES MODELS
175
166
  function formatGrepOutput(result: GrepResult): string {
176
167
  if (result.items.length === 0) return "No matches found";
177
168
 
@@ -179,7 +170,6 @@ function formatGrepOutput(result: GrepResult): string {
179
170
  // This preserves native frecency ordering across files without re-sorting.
180
171
  const lines: string[] = [];
181
172
  let currentFile = "";
182
- let shown = 0;
183
173
 
184
174
  for (const match of result.items) {
185
175
  if (match.relativePath !== currentFile) {
@@ -194,7 +184,6 @@ function formatGrepOutput(result: GrepResult): string {
194
184
  });
195
185
 
196
186
  lines.push(` ${match.lineNumber}: ${truncateLine(match.lineContent)}`);
197
- shown++;
198
187
 
199
188
  match.contextAfter?.forEach((line: string, i: number) => {
200
189
  const lineNum = match.lineNumber + 1 + i;
@@ -318,15 +307,14 @@ export default function fffExtension(pi: ExtensionAPI) {
318
307
 
319
308
  const toolNames = resolveToolNames(currentMode);
320
309
 
321
- // DB path resolution: flag > env > undefined (no persistent DBs)
322
- const frecencyDbPath =
323
- (pi.getFlag("fff-frecency-db") as string | undefined) ??
324
- process.env.FFF_FRECENCY_DB ??
325
- undefined;
326
- const historyDbPath =
327
- (pi.getFlag("fff-history-db") as string | undefined) ??
328
- process.env.FFF_HISTORY_DB ??
329
- undefined;
310
+ // DB path resolution: flag > env > existing fff.nvim db > pi-local data dir.
311
+ const resolvedDbPaths = resolveDbPaths({
312
+ frecency:
313
+ (pi.getFlag("fff-frecency-db") as string | undefined) ??
314
+ process.env.FFF_FRECENCY_DB,
315
+ history:
316
+ (pi.getFlag("fff-history-db") as string | undefined) ?? process.env.FFF_HISTORY_DB,
317
+ });
330
318
 
331
319
  // flag (boolean) > env ("1"/"true", or "0"/"false") > default.
332
320
  function resolveBoolOpt(flagName: string, envName: string, fallback = false): boolean {
@@ -380,12 +368,21 @@ export default function fffExtension(pi: ExtensionAPI) {
380
368
  );
381
369
  }
382
370
 
371
+ const pickers = new FilePickerFactory({
372
+ frecencyDbPath: resolvedDbPaths.frecency,
373
+ historyDbPath: resolvedDbPaths.history,
374
+ onDbFailure: (error) =>
375
+ uiCtx?.ui.notify(
376
+ `(fff): Failed to open frecency/history database (${error}). Continuing without frecency persistence.`,
377
+ "error",
378
+ ),
379
+ });
380
+
383
381
  const auxPool = new AuxFinderPool({
384
382
  enableFsRootScanning,
385
383
  enableHomeDirScanning,
386
384
  onHomeDirScan: warnHomeDirScan,
387
- frecencyDbPath,
388
- historyDbPath,
385
+ pickers,
389
386
  });
390
387
 
391
388
  // in case cwd changes we need to figure this out
@@ -402,22 +399,14 @@ export default function fffExtension(pi: ExtensionAPI) {
402
399
  finderCwd = null;
403
400
  }
404
401
 
405
- const { FileFinder } = await loadSdk();
406
- const result = FileFinder.create({
402
+ // if the dbs can't be opened the factory falls back to a db-less picker,
403
+ // e.g. when some other process corrupts the lock
404
+ mainFinder = await pickers.create({
407
405
  basePath: cwd,
408
- frecencyDbPath,
409
- historyDbPath,
410
- aiMode: true,
411
406
  enableHomeDirScanning,
412
407
  enableFsRootScanning,
413
408
  });
414
-
415
- if (!result.ok)
416
- throw new Error(`Failed to create FFF file finder: ${result.error}`);
417
-
418
- mainFinder = result.value;
419
409
  finderCwd = cwd;
420
- await mainFinder.waitForScan(SCAN_TIMEOUT_MS);
421
410
  return mainFinder;
422
411
  })().finally(() => {
423
412
  finderPromise = null;
package/src/paths.ts CHANGED
@@ -1,9 +1,67 @@
1
+ import fs from "node:fs";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  // Resolved once per process: os.homedir() hits the env/passwd on every call.
5
6
  export const HOME_DIR = path.resolve(os.homedir());
6
7
 
8
+ // fff.nvim db dir names (`frecency.db_path` / `history.db_path` in lua/fff/conf.lua).
9
+ const NVIM_FRECENCY_DIR = "fff_nvim";
10
+ const NVIM_HISTORY_DIR = "fff_queries";
11
+
12
+ export interface DbPaths {
13
+ frecency: string;
14
+ history: string;
15
+ }
16
+
7
17
  export function isHomeDir(dir: string): boolean {
8
18
  return path.resolve(dir) === HOME_DIR;
9
19
  }
20
+
21
+ // Resolution order: explicit override > existing fff.nvim db > pi-local data dir.
22
+ // Reusing the nvim db lets pi rank files by the frecency the user built in their editor.
23
+ export function resolveDbPaths(overrides: {
24
+ frecency?: string;
25
+ history?: string;
26
+ }): DbPaths {
27
+ return {
28
+ frecency:
29
+ overrides.frecency ??
30
+ existingDir(nvimCacheDir(), NVIM_FRECENCY_DIR) ??
31
+ path.join(piDataDir(), "fff", "frecency"),
32
+ history:
33
+ overrides.history ??
34
+ existingDir(nvimDataDir(), NVIM_HISTORY_DIR) ??
35
+ path.join(piDataDir(), "fff", "history"),
36
+ };
37
+ }
38
+
39
+ function nvimCacheDir(): string {
40
+ const xdg = process.env.XDG_CACHE_HOME;
41
+ if (xdg) return path.join(xdg, "nvim");
42
+ if (process.platform === "win32" && process.env.LOCALAPPDATA)
43
+ return path.join(process.env.LOCALAPPDATA, "nvim-data", "cache");
44
+ return path.join(HOME_DIR, ".cache", "nvim");
45
+ }
46
+
47
+ function nvimDataDir(): string {
48
+ const xdg = process.env.XDG_DATA_HOME;
49
+ if (xdg) return path.join(xdg, "nvim");
50
+ if (process.platform === "win32" && process.env.LOCALAPPDATA)
51
+ return path.join(process.env.LOCALAPPDATA, "nvim-data");
52
+ return path.join(HOME_DIR, ".local", "share", "nvim");
53
+ }
54
+
55
+ function piDataDir(): string {
56
+ return process.env.PI_CODING_AGENT_DIR ?? path.join(HOME_DIR, ".pi", "agent");
57
+ }
58
+
59
+ // LMDB environments are directories, so a stray file at the same path is not a db.
60
+ function existingDir(parent: string, name: string): string | undefined {
61
+ const candidate = path.join(parent, name);
62
+ try {
63
+ return fs.statSync(candidate).isDirectory() ? candidate : undefined;
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }