@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 +17 -5
- package/package.json +4 -3
- package/src/aux-finders.ts +4 -18
- package/src/file-picker.ts +73 -0
- package/src/index.ts +25 -36
- package/src/paths.ts +58 -0
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
|
-
|
|
143
|
-
- frecency database
|
|
144
|
-
- history database
|
|
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-
|
|
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-
|
|
44
|
-
"@ff-labs/fff-node": "0.10.5-
|
|
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
|
}
|
package/src/aux-finders.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
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 >
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
+
}
|