@ff-labs/pi-fff 0.11.1-nightly.89c1927 → 0.11.1-nightly.a377069

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/package.json +3 -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.1-nightly.89c1927",
4
+ "version": "0.11.1-nightly.a377069",
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.1-nightly.89c1927",
45
- "@ff-labs/fff-node": "0.11.1-nightly.89c1927"
44
+ "@ff-labs/fff-bun": "0.11.1-nightly.a377069",
45
+ "@ff-labs/fff-node": "0.11.1-nightly.a377069"
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
- const cursorCache = new Map<string, GrepCursor>();
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: GrepCursor): string {
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): GrepCursor | undefined {
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 aux = await resolveFinderForPath(params.path, pattern, params.exclude);
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 = aux ? aux.finder : await ensureFinder(activeCwd);
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
- const query = aux
1019
- ? aux.query
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
- const grepResult = picker.grep(query, {
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: (params.cursor ? getCursor(params.cursor) : null) ?? null,
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: true,
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
- notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`);
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 f = await ensureFinder(activeCwd);
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
- const grepResult = f.multiGrep({
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: (params.cursor ? getCursor(params.cursor) : null) ?? null,
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
- const result = grepResult.value;
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
- notices.push(
1365
- `More available. cursor="${storeCursor(result.nextCursor)}" to continue`,
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 }],