@ff-labs/pi-fff 0.10.5 → 0.10.6-nightly.be2dd8d
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 +33 -3
- package/package.json +3 -3
- package/src/config.ts +100 -0
- package/src/index.ts +58 -28
- package/src/paths.ts +1 -1
package/README.md
CHANGED
|
@@ -127,7 +127,36 @@ Parameters:
|
|
|
127
127
|
Mode precedence:
|
|
128
128
|
1. `--fff-mode <mode>` CLI flag
|
|
129
129
|
2. `PI_FFF_MODE=<mode>` environment variable
|
|
130
|
-
3.
|
|
130
|
+
3. `mode` in the global config file
|
|
131
|
+
4. default (`tools-and-ui`)
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
For persistent global configuration, create `pi-fff.json` in pi's agent directory (`~/.pi/agent/pi-fff.json` by default; `PI_CODING_AGENT_DIR` is respected):
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mode": "override",
|
|
140
|
+
"frecencyDbPath": "/path/to/frecency",
|
|
141
|
+
"historyDbPath": "/path/to/history",
|
|
142
|
+
"enableFsRootScanning": false,
|
|
143
|
+
"enableHomeDirScanning": true
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
All fields are optional:
|
|
148
|
+
|
|
149
|
+
| Field | Type | Default |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `mode` | `tools-and-ui`, `tools-only`, or `override` | `tools-and-ui` |
|
|
152
|
+
| `frecencyDbPath` | non-empty string | See [Data](#data) |
|
|
153
|
+
| `historyDbPath` | non-empty string | See [Data](#data) |
|
|
154
|
+
| `enableFsRootScanning` | boolean | `false` |
|
|
155
|
+
| `enableHomeDirScanning` | boolean | `true` |
|
|
156
|
+
|
|
157
|
+
CLI flags take precedence over environment variables, which take precedence over this file. A missing file is ignored. Malformed JSON, unknown fields, and invalid values stop the extension from loading and report the file path and error. `/fff-mode` changes the current session; it does not edit this file.
|
|
158
|
+
|
|
159
|
+
The file is global only. Project-level config cannot safely control tool names because pi decides which tools an extension registers before project configuration can be trusted.
|
|
131
160
|
|
|
132
161
|
## Flags
|
|
133
162
|
|
|
@@ -147,11 +176,12 @@ Each path is resolved independently, in this order:
|
|
|
147
176
|
|
|
148
177
|
1. CLI flag — `--fff-frecency-db` / `--fff-history-db`
|
|
149
178
|
2. Env var — `FFF_FRECENCY_DB` / `FFF_HISTORY_DB`
|
|
150
|
-
3.
|
|
179
|
+
3. Global config — `frecencyDbPath` / `historyDbPath`
|
|
180
|
+
4. An existing [fff.nvim](https://github.com/dmtrKovalenko/fff.nvim) database, so pi reuses the frecency you built up in your editor:
|
|
151
181
|
- frecency: `$XDG_CACHE_HOME/nvim/fff_nvim`
|
|
152
182
|
- history: `$XDG_DATA_HOME/nvim/fff_queries`
|
|
153
183
|
- `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
|
-
|
|
184
|
+
5. pi-local directory, created on demand — `$PI_CODING_AGENT_DIR/fff/{frecency,history}`, defaulting to `~/.pi/agent/fff/{frecency,history}`
|
|
155
185
|
|
|
156
186
|
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.
|
|
157
187
|
|
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.
|
|
4
|
+
"version": "0.10.6-nightly.be2dd8d",
|
|
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.
|
|
44
|
-
"@ff-labs/fff-node": "0.10.
|
|
43
|
+
"@ff-labs/fff-bun": "0.10.6-nightly.be2dd8d",
|
|
44
|
+
"@ff-labs/fff-node": "0.10.6-nightly.be2dd8d"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { piDataDir } from "./paths";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_FILE_NAME = "pi-fff.json";
|
|
6
|
+
export const VALID_MODES = ["tools-and-ui", "tools-only", "override"] as const;
|
|
7
|
+
|
|
8
|
+
export type FffMode = (typeof VALID_MODES)[number];
|
|
9
|
+
|
|
10
|
+
export interface FffConfig {
|
|
11
|
+
mode?: FffMode;
|
|
12
|
+
frecencyDbPath?: string;
|
|
13
|
+
historyDbPath?: string;
|
|
14
|
+
enableFsRootScanning?: boolean;
|
|
15
|
+
enableHomeDirScanning?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CONFIG_KEYS = new Set<keyof FffConfig>([
|
|
19
|
+
"mode",
|
|
20
|
+
"frecencyDbPath",
|
|
21
|
+
"historyDbPath",
|
|
22
|
+
"enableFsRootScanning",
|
|
23
|
+
"enableHomeDirScanning",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export function loadConfig(agentDir = piDataDir()): FffConfig {
|
|
27
|
+
const configPath = join(agentDir, CONFIG_FILE_NAME);
|
|
28
|
+
let contents: string;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
contents = readFileSync(configPath, "utf8");
|
|
32
|
+
} catch (error: unknown) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Could not read pi-fff config at ${configPath}: ${errorMessage(error)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(contents);
|
|
42
|
+
} catch (error: unknown) {
|
|
43
|
+
throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isRecord(parsed)) {
|
|
47
|
+
throw invalidConfig(configPath, "expected a JSON object");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const key of Object.keys(parsed)) {
|
|
51
|
+
if (!CONFIG_KEYS.has(key as keyof FffConfig)) {
|
|
52
|
+
throw invalidConfig(configPath, `unknown option "${key}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (parsed.mode !== undefined && !VALID_MODES.includes(parsed.mode as FffMode)) {
|
|
57
|
+
throw invalidConfig(configPath, `"mode" must be one of ${VALID_MODES.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
validateString(configPath, parsed, "frecencyDbPath");
|
|
61
|
+
validateString(configPath, parsed, "historyDbPath");
|
|
62
|
+
validateBoolean(configPath, parsed, "enableFsRootScanning");
|
|
63
|
+
validateBoolean(configPath, parsed, "enableHomeDirScanning");
|
|
64
|
+
|
|
65
|
+
return parsed as FffConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function invalidConfig(configPath: string, reason: string): Error {
|
|
69
|
+
return new Error(`Invalid pi-fff config at ${configPath}: ${reason}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function errorMessage(error: unknown): string {
|
|
73
|
+
return error instanceof Error ? error.message : String(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateString(
|
|
81
|
+
configPath: string,
|
|
82
|
+
config: Record<string, unknown>,
|
|
83
|
+
key: "frecencyDbPath" | "historyDbPath",
|
|
84
|
+
): void {
|
|
85
|
+
const value = config[key];
|
|
86
|
+
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
|
|
87
|
+
throw invalidConfig(configPath, `"${key}" must be a non-empty string`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateBoolean(
|
|
92
|
+
configPath: string,
|
|
93
|
+
config: Record<string, unknown>,
|
|
94
|
+
key: "enableFsRootScanning" | "enableHomeDirScanning",
|
|
95
|
+
): void {
|
|
96
|
+
const value = config[key];
|
|
97
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
98
|
+
throw invalidConfig(configPath, `"${key}" must be a boolean`);
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ 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 { type FffMode, loadConfig, VALID_MODES } from "./config";
|
|
25
26
|
import { FilePickerFactory } from "./file-picker";
|
|
26
27
|
import { isHomeDir, resolveDbPaths } from "./paths";
|
|
27
28
|
import { buildQuery } from "./query";
|
|
@@ -45,11 +46,7 @@ const GREP_TIME_BUDGET_MS = 10_000;
|
|
|
45
46
|
const HOME_SCAN_STATUS_KEY = "fff";
|
|
46
47
|
const HOME_SCAN_POLL_MS = 1_000;
|
|
47
48
|
const HOME_SCAN_DISABLE_HINT =
|
|
48
|
-
"You can prevent home dir indexing with --fff-enable-home-scan=false
|
|
49
|
-
|
|
50
|
-
type FffMode = "tools-and-ui" | "tools-only" | "override";
|
|
51
|
-
|
|
52
|
-
const VALID_MODES: FffMode[] = ["tools-and-ui", "tools-only", "override"];
|
|
49
|
+
"You can prevent home dir indexing with --fff-enable-home-scan=false, FFF_ENABLE_HOME_SCAN=0, or enableHomeDirScanning in pi-fff.json.";
|
|
53
50
|
|
|
54
51
|
interface ToolNames {
|
|
55
52
|
grep: string;
|
|
@@ -299,44 +296,77 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
299
296
|
let finderPromise: Promise<FileFinderApi> | null = null;
|
|
300
297
|
let activeCwd = process.cwd();
|
|
301
298
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
const config = loadConfig();
|
|
300
|
+
|
|
301
|
+
// All startup options use the same flag > env > file > fallback order.
|
|
302
|
+
function getConfigValue<T>(
|
|
303
|
+
flagName: string,
|
|
304
|
+
envName: string,
|
|
305
|
+
fileValue: T | undefined,
|
|
306
|
+
fallback: T,
|
|
307
|
+
parse: (value: unknown) => T | undefined = (value) => value as T,
|
|
308
|
+
): T {
|
|
309
|
+
const flagValue = pi.getFlag(flagName);
|
|
310
|
+
if (flagValue !== undefined) {
|
|
311
|
+
const value = parse(flagValue);
|
|
312
|
+
if (value !== undefined) return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const envValue = process.env[envName];
|
|
316
|
+
if (envValue !== undefined) {
|
|
317
|
+
const value = parse(envValue);
|
|
318
|
+
if (value !== undefined) return value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return fileValue ?? fallback;
|
|
322
|
+
}
|
|
307
323
|
|
|
324
|
+
function parseBoolean(value: unknown): boolean | undefined {
|
|
325
|
+
if (typeof value === "boolean") return value;
|
|
326
|
+
if (value === "1" || value === "true") return true;
|
|
327
|
+
if (value === "0" || value === "false") return false;
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let currentMode = getConfigValue(
|
|
332
|
+
"fff-mode",
|
|
333
|
+
"PI_FFF_MODE",
|
|
334
|
+
config.mode,
|
|
335
|
+
"tools-and-ui",
|
|
336
|
+
);
|
|
308
337
|
const toolNames = resolveToolNames(currentMode);
|
|
309
338
|
|
|
310
|
-
// DB path resolution: flag > env > existing fff.nvim db > pi-local data dir.
|
|
311
339
|
const resolvedDbPaths = resolveDbPaths({
|
|
312
|
-
frecency:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
frecency: getConfigValue(
|
|
341
|
+
"fff-frecency-db",
|
|
342
|
+
"FFF_FRECENCY_DB",
|
|
343
|
+
config.frecencyDbPath,
|
|
344
|
+
undefined,
|
|
345
|
+
),
|
|
346
|
+
history: getConfigValue(
|
|
347
|
+
"fff-history-db",
|
|
348
|
+
"FFF_HISTORY_DB",
|
|
349
|
+
config.historyDbPath,
|
|
350
|
+
undefined,
|
|
351
|
+
),
|
|
317
352
|
});
|
|
318
353
|
|
|
319
|
-
// flag (boolean) > env ("1"/"true", or "0"/"false") > default.
|
|
320
|
-
function resolveBoolOpt(flagName: string, envName: string, fallback = false): boolean {
|
|
321
|
-
const flag = pi.getFlag(flagName);
|
|
322
|
-
if (typeof flag === "boolean") return flag;
|
|
323
|
-
if (typeof flag === "string") return flag === "true" || flag === "1";
|
|
324
|
-
const env = process.env[envName];
|
|
325
|
-
if (env === "1" || env === "true") return true;
|
|
326
|
-
if (env === "0" || env === "false") return false;
|
|
327
|
-
return fallback;
|
|
328
|
-
}
|
|
329
354
|
// Root scanning opt-in: FFF refuses to init at / unless this is set.
|
|
330
|
-
const enableFsRootScanning =
|
|
355
|
+
const enableFsRootScanning = getConfigValue(
|
|
331
356
|
"fff-enable-root-scan",
|
|
332
357
|
"FFF_ENABLE_ROOT_SCAN",
|
|
358
|
+
config.enableFsRootScanning,
|
|
359
|
+
false,
|
|
360
|
+
parseBoolean,
|
|
333
361
|
);
|
|
334
362
|
// Home dir scanning is on by default (launching pi from $HOME is a normal
|
|
335
363
|
// flow), but configurable so users with huge $HOME trees can opt out.
|
|
336
|
-
const enableHomeDirScanning =
|
|
364
|
+
const enableHomeDirScanning = getConfigValue(
|
|
337
365
|
"fff-enable-home-scan",
|
|
338
366
|
"FFF_ENABLE_HOME_SCAN",
|
|
367
|
+
config.enableHomeDirScanning,
|
|
339
368
|
true,
|
|
369
|
+
parseBoolean,
|
|
340
370
|
);
|
|
341
371
|
|
|
342
372
|
function getMode(): FffMode {
|
package/src/paths.ts
CHANGED
|
@@ -52,7 +52,7 @@ function nvimDataDir(): string {
|
|
|
52
52
|
return path.join(HOME_DIR, ".local", "share", "nvim");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function piDataDir(): string {
|
|
55
|
+
export function piDataDir(): string {
|
|
56
56
|
return process.env.PI_CODING_AGENT_DIR ?? path.join(HOME_DIR, ".pi", "agent");
|
|
57
57
|
}
|
|
58
58
|
|