@ff-labs/pi-fff 0.11.0 → 0.11.1-nightly.1772109
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 +4 -0
- package/package.json +3 -3
- package/src/index.ts +129 -24
package/README.md
CHANGED
|
@@ -83,8 +83,11 @@ This extension registers FFF-powered tools (`fffind`, `ffgrep`, `fff-multi-grep`
|
|
|
83
83
|
|
|
84
84
|
Search file contents. Smart case, plain text by default, regex optional.
|
|
85
85
|
|
|
86
|
+
Set `definitions_only: true` to search for matching declarations. If the completed search finds none, the tool reports that and returns ordinary matches. Cursors remember the search or fallback state. The optional `fff-multi-grep` tool accepts the same flag. Omitting the flag keeps ordinary search behavior.
|
|
87
|
+
|
|
86
88
|
Parameters:
|
|
87
89
|
- `pattern` — search text or regex
|
|
90
|
+
- `definitions_only` — prefer declarations; fall back to ordinary matches with a notice when none exist
|
|
88
91
|
- `path` — directory/file constraint (e.g. `src/`, `*.ts`)
|
|
89
92
|
- `ignoreCase` — force case-insensitive
|
|
90
93
|
- `literal` — treat as literal string (default: true)
|
|
@@ -107,6 +110,7 @@ OR-logic multi-pattern content search. SIMD-accelerated Aho-Corasick.
|
|
|
107
110
|
|
|
108
111
|
Parameters:
|
|
109
112
|
- `patterns` — array of literal patterns (OR logic)
|
|
113
|
+
- `definitions_only` — prefer declarations with the same fallback behavior
|
|
110
114
|
- `constraints` — file constraints (e.g. `*.{ts,tsx} !test/`)
|
|
111
115
|
- `context` — context lines
|
|
112
116
|
- `limit` — max matches (default: 100)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/pi-fff",
|
|
3
3
|
"public": true,
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.1-nightly.1772109",
|
|
5
5
|
"description": "pi extension: FFF-powered fuzzy file and content search",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@ff-labs/fff-bun": "0.11.
|
|
45
|
-
"@ff-labs/fff-node": "0.11.
|
|
44
|
+
"@ff-labs/fff-bun": "0.11.1-nightly.1772109",
|
|
45
|
+
"@ff-labs/fff-node": "0.11.1-nightly.1772109"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/index.ts
CHANGED
|
@@ -89,10 +89,19 @@ function toolNameList(names: ToolNames): string[] {
|
|
|
89
89
|
// Cursor store — simple bounded Map for pagination cursors
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
interface ToolGrepCursor {
|
|
93
|
+
cursor: GrepCursor;
|
|
94
|
+
finder: FileFinderApi;
|
|
95
|
+
definitionsOnly: boolean;
|
|
96
|
+
foundDefinitions: boolean;
|
|
97
|
+
mode?: GrepMode;
|
|
98
|
+
query?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cursorCache = new Map<string, ToolGrepCursor>();
|
|
93
102
|
let cursorCounter = 0;
|
|
94
103
|
|
|
95
|
-
function storeCursor(cursor:
|
|
104
|
+
function storeCursor(cursor: ToolGrepCursor): string {
|
|
96
105
|
const id = `fff_c${++cursorCounter}`;
|
|
97
106
|
cursorCache.set(id, cursor);
|
|
98
107
|
if (cursorCache.size > 200) {
|
|
@@ -102,7 +111,7 @@ function storeCursor(cursor: GrepCursor): string {
|
|
|
102
111
|
return id;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
function getCursor(id: string):
|
|
114
|
+
function getCursor(id: string): ToolGrepCursor | undefined {
|
|
106
115
|
return cursorCache.get(id);
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -954,7 +963,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
954
963
|
|
|
955
964
|
// --- grep tool ---
|
|
956
965
|
|
|
966
|
+
const definitionsOnlySchema = Type.Optional(
|
|
967
|
+
Type.Boolean({
|
|
968
|
+
description:
|
|
969
|
+
"Return matching declarations. If none are found, report that and return other matches.",
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
|
|
957
973
|
const grepSchema = Type.Object({
|
|
974
|
+
definitions_only: definitionsOnlySchema,
|
|
958
975
|
pattern: Type.String({
|
|
959
976
|
description: "Search pattern (literal text or regex)",
|
|
960
977
|
}),
|
|
@@ -996,6 +1013,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
996
1013
|
promptSnippet: "Grep contents",
|
|
997
1014
|
promptGuidelines: (names) => [
|
|
998
1015
|
`${names.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
|
1016
|
+
`${names.grep}: use definitions_only: true to find declarations; if none are found, the tool returns other matches with a notice.`,
|
|
999
1017
|
`${names.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
|
1000
1018
|
`${names.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
|
1001
1019
|
`${names.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
|
@@ -1006,25 +1024,31 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1006
1024
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1007
1025
|
|
|
1008
1026
|
const pattern = params.pattern;
|
|
1009
|
-
const
|
|
1027
|
+
const resumed = params.cursor ? getCursor(params.cursor) : undefined;
|
|
1028
|
+
const aux = resumed
|
|
1029
|
+
? null
|
|
1030
|
+
: await resolveFinderForPath(params.path, pattern, params.exclude);
|
|
1010
1031
|
|
|
1011
|
-
const picker =
|
|
1032
|
+
const picker =
|
|
1033
|
+
resumed?.finder ?? (aux ? aux.finder : await ensureFinder(activeCwd));
|
|
1034
|
+
if (picker.isDestroyed)
|
|
1035
|
+
throw new Error("Search cursor expired. Start a new search without the cursor.");
|
|
1012
1036
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
1013
1037
|
// pageSize caps TOTAL matches across all files (soft cap: the current file
|
|
1014
1038
|
// is always finished first). maxMatchesPerFile stays decoupled at the engine
|
|
1015
1039
|
// default so same-file overflow remains reachable via cursor (#825).
|
|
1016
1040
|
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
|
1017
1041
|
const context = clampContext(params.context);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
: buildQuery(params.path, pattern, params.exclude, activeCwd);
|
|
1042
|
+
let query =
|
|
1043
|
+
resumed?.query ??
|
|
1044
|
+
(aux ? aux.query : buildQuery(params.path, pattern, params.exclude, activeCwd));
|
|
1021
1045
|
|
|
1022
1046
|
// Auto-detect: regex if the pattern has regex metacharacters AND parses
|
|
1023
1047
|
// as a valid regex, otherwise plain literal. The fuzzy fallback below
|
|
1024
1048
|
// only kicks in for plain mode — regex queries are intentional.
|
|
1025
1049
|
const hasRegexSyntax = pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1026
1050
|
|
|
1027
|
-
let mode: GrepMode = hasRegexSyntax ? "regex" : "plain";
|
|
1051
|
+
let mode: GrepMode = resumed?.mode ?? (hasRegexSyntax ? "regex" : "plain");
|
|
1028
1052
|
if (mode === "regex") {
|
|
1029
1053
|
try {
|
|
1030
1054
|
new RegExp(pattern);
|
|
@@ -1059,21 +1083,42 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1059
1083
|
// (case-insensitive when pattern is all lowercase).
|
|
1060
1084
|
const smartCase = params.caseSensitive !== true;
|
|
1061
1085
|
|
|
1062
|
-
|
|
1086
|
+
let definitionsOnly = resumed?.definitionsOnly ?? params.definitions_only ?? false;
|
|
1087
|
+
const preferDefinitions = definitionsOnly;
|
|
1088
|
+
const foundDefinitions = resumed?.foundDefinitions ?? false;
|
|
1089
|
+
const options = {
|
|
1063
1090
|
mode,
|
|
1064
1091
|
smartCase,
|
|
1065
1092
|
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
1066
1093
|
pageSize,
|
|
1067
|
-
cursor:
|
|
1094
|
+
cursor: resumed?.cursor ?? null,
|
|
1068
1095
|
beforeContext: context,
|
|
1069
1096
|
afterContext: context,
|
|
1070
|
-
classifyDefinitions: true,
|
|
1071
1097
|
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
1098
|
+
};
|
|
1099
|
+
const grepResult = picker.grep(query, {
|
|
1100
|
+
...options,
|
|
1101
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "classify",
|
|
1072
1102
|
});
|
|
1073
1103
|
|
|
1074
1104
|
if (!grepResult.ok) throw new Error(grepResult.error);
|
|
1075
1105
|
|
|
1076
1106
|
let result = grepResult.value;
|
|
1107
|
+
const definitionsFallback =
|
|
1108
|
+
definitionsOnly &&
|
|
1109
|
+
!foundDefinitions &&
|
|
1110
|
+
result.items.length === 0 &&
|
|
1111
|
+
!result.nextCursor;
|
|
1112
|
+
if (definitionsFallback) {
|
|
1113
|
+
const fallback = picker.grep(query, {
|
|
1114
|
+
...options,
|
|
1115
|
+
cursor: null,
|
|
1116
|
+
classifyDefinitions: "classify",
|
|
1117
|
+
});
|
|
1118
|
+
if (!fallback.ok) throw new Error(fallback.error);
|
|
1119
|
+
result = fallback.value;
|
|
1120
|
+
definitionsOnly = false;
|
|
1121
|
+
}
|
|
1077
1122
|
let fuzzyNotice: string | null = null;
|
|
1078
1123
|
|
|
1079
1124
|
// if we hit the timeout do not run the fuzzy fallback
|
|
@@ -1081,7 +1126,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1081
1126
|
if (
|
|
1082
1127
|
result.items.length === 0 &&
|
|
1083
1128
|
!result.nextCursor &&
|
|
1084
|
-
!params.cursor &&
|
|
1129
|
+
(!params.cursor || definitionsFallback) &&
|
|
1085
1130
|
mode !== "regex"
|
|
1086
1131
|
) {
|
|
1087
1132
|
// When the caller pinned a specific file (path has an extension), the
|
|
@@ -1091,7 +1136,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1091
1136
|
// excluded / out-of-scope directories.
|
|
1092
1137
|
const lastSeg = params.path?.split(/[\\/]/).pop() ?? "";
|
|
1093
1138
|
const pathTargetsFile = /\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSeg);
|
|
1094
|
-
const fuzzyQuery = pathTargetsFile ? pattern : query;
|
|
1139
|
+
const fuzzyQuery = pathTargetsFile && !preferDefinitions ? pattern : query;
|
|
1095
1140
|
const fuzzy = picker.grep(fuzzyQuery, {
|
|
1096
1141
|
mode: "fuzzy",
|
|
1097
1142
|
smartCase,
|
|
@@ -1100,13 +1145,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1100
1145
|
cursor: null,
|
|
1101
1146
|
beforeContext: 0,
|
|
1102
1147
|
afterContext: 0,
|
|
1103
|
-
classifyDefinitions:
|
|
1148
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "classify",
|
|
1104
1149
|
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
1105
1150
|
});
|
|
1106
1151
|
|
|
1107
1152
|
if (fuzzy.ok && fuzzy.value.items.length > 0) {
|
|
1108
1153
|
fuzzyNotice = `0 exact matches. Maybe you meant this?`;
|
|
1109
1154
|
result = fuzzy.value;
|
|
1155
|
+
mode = "fuzzy";
|
|
1156
|
+
query = fuzzyQuery;
|
|
1110
1157
|
}
|
|
1111
1158
|
}
|
|
1112
1159
|
|
|
@@ -1116,11 +1163,27 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1116
1163
|
notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`);
|
|
1117
1164
|
}
|
|
1118
1165
|
if (result.nextCursor) {
|
|
1119
|
-
|
|
1166
|
+
const cursor = storeCursor({
|
|
1167
|
+
cursor: result.nextCursor,
|
|
1168
|
+
finder: picker,
|
|
1169
|
+
definitionsOnly,
|
|
1170
|
+
foundDefinitions:
|
|
1171
|
+
foundDefinitions || (definitionsOnly && result.items.length > 0),
|
|
1172
|
+
mode,
|
|
1173
|
+
query,
|
|
1174
|
+
});
|
|
1175
|
+
notices.push(`Continue with cursor="${cursor}"`);
|
|
1120
1176
|
}
|
|
1121
1177
|
|
|
1122
1178
|
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
1123
1179
|
if (fuzzyNotice) output = `[${fuzzyNotice}]\n${output}`;
|
|
1180
|
+
if (definitionsFallback) {
|
|
1181
|
+
const notice =
|
|
1182
|
+
result.items.length > 0 || result.nextCursor
|
|
1183
|
+
? "No matching definitions found. Showing other matches:"
|
|
1184
|
+
: "No matching definitions or other matches found.";
|
|
1185
|
+
output = `[${notice}]\n${output}`;
|
|
1186
|
+
}
|
|
1124
1187
|
|
|
1125
1188
|
return {
|
|
1126
1189
|
content: [{ type: "text", text: output }],
|
|
@@ -1300,6 +1363,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1300
1363
|
|
|
1301
1364
|
if (enableMultiGrep) {
|
|
1302
1365
|
const multiGrepSchema = Type.Object({
|
|
1366
|
+
definitions_only: definitionsOnlySchema,
|
|
1303
1367
|
patterns: Type.Array(Type.String(), {
|
|
1304
1368
|
description:
|
|
1305
1369
|
"Literal patterns (OR). Include snake_case/camelCase/PascalCase variants.",
|
|
@@ -1328,6 +1392,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1328
1392
|
`${names.multiGrep}: use when searching for several identifiers at once.`,
|
|
1329
1393
|
`${names.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
|
1330
1394
|
`${names.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
|
1395
|
+
`${names.multiGrep}: use definitions_only: true to find declarations; if none are found, the tool returns other matches with a notice.`,
|
|
1331
1396
|
],
|
|
1332
1397
|
parameters: multiGrepSchema,
|
|
1333
1398
|
|
|
@@ -1336,36 +1401,76 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1336
1401
|
if (!params.patterns?.length)
|
|
1337
1402
|
throw new Error("patterns array must have at least 1 element");
|
|
1338
1403
|
|
|
1339
|
-
const
|
|
1404
|
+
const resumed = params.cursor ? getCursor(params.cursor) : undefined;
|
|
1405
|
+
const f = resumed?.finder ?? (await ensureFinder(activeCwd));
|
|
1406
|
+
if (f.isDestroyed)
|
|
1407
|
+
throw new Error(
|
|
1408
|
+
"Search cursor expired. Start a new search without the cursor.",
|
|
1409
|
+
);
|
|
1340
1410
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
1341
1411
|
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
|
1342
1412
|
const context = clampContext(params.context);
|
|
1343
1413
|
|
|
1344
|
-
|
|
1414
|
+
let definitionsOnly =
|
|
1415
|
+
resumed?.definitionsOnly ?? params.definitions_only ?? false;
|
|
1416
|
+
const foundDefinitions = resumed?.foundDefinitions ?? false;
|
|
1417
|
+
const options = {
|
|
1345
1418
|
patterns: params.patterns,
|
|
1346
1419
|
constraints: params.constraints,
|
|
1347
1420
|
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
1348
1421
|
pageSize,
|
|
1349
1422
|
smartCase: true,
|
|
1350
|
-
cursor:
|
|
1423
|
+
cursor: resumed?.cursor ?? null,
|
|
1351
1424
|
beforeContext: context,
|
|
1352
1425
|
afterContext: context,
|
|
1426
|
+
};
|
|
1427
|
+
const grepResult = f.multiGrep({
|
|
1428
|
+
...options,
|
|
1429
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "off",
|
|
1353
1430
|
});
|
|
1354
1431
|
|
|
1355
1432
|
if (!grepResult.ok) throw new Error(grepResult.error);
|
|
1356
1433
|
|
|
1357
|
-
|
|
1434
|
+
let result = grepResult.value;
|
|
1435
|
+
const definitionsFallback =
|
|
1436
|
+
definitionsOnly &&
|
|
1437
|
+
!foundDefinitions &&
|
|
1438
|
+
result.items.length === 0 &&
|
|
1439
|
+
!result.nextCursor;
|
|
1440
|
+
if (definitionsFallback) {
|
|
1441
|
+
const fallback = f.multiGrep({
|
|
1442
|
+
...options,
|
|
1443
|
+
cursor: null,
|
|
1444
|
+
classifyDefinitions: "off",
|
|
1445
|
+
});
|
|
1446
|
+
if (!fallback.ok) throw new Error(fallback.error);
|
|
1447
|
+
result = fallback.value;
|
|
1448
|
+
definitionsOnly = false;
|
|
1449
|
+
}
|
|
1358
1450
|
let output = formatGrepOutput(result);
|
|
1359
1451
|
|
|
1360
1452
|
const notices: string[] = [];
|
|
1361
1453
|
if (result.items.length >= effectiveLimit)
|
|
1362
1454
|
notices.push(`${effectiveLimit}+ matches (refine patterns)`);
|
|
1363
|
-
if (result.nextCursor)
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1455
|
+
if (result.nextCursor) {
|
|
1456
|
+
const cursor = storeCursor({
|
|
1457
|
+
cursor: result.nextCursor,
|
|
1458
|
+
finder: f,
|
|
1459
|
+
definitionsOnly,
|
|
1460
|
+
foundDefinitions:
|
|
1461
|
+
foundDefinitions || (definitionsOnly && result.items.length > 0),
|
|
1462
|
+
});
|
|
1463
|
+
notices.push(`More available. cursor="${cursor}" to continue`);
|
|
1464
|
+
}
|
|
1367
1465
|
|
|
1368
1466
|
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
1467
|
+
if (definitionsFallback) {
|
|
1468
|
+
const notice =
|
|
1469
|
+
result.items.length > 0 || result.nextCursor
|
|
1470
|
+
? "No matching definitions found. Showing other matches:"
|
|
1471
|
+
: "No matching definitions or other matches found.";
|
|
1472
|
+
output = `[${notice}]\n${output}`;
|
|
1473
|
+
}
|
|
1369
1474
|
|
|
1370
1475
|
return {
|
|
1371
1476
|
content: [{ type: "text", text: output }],
|