pi-readseek 0.12.0 → 0.12.1

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/dist/index.ts CHANGED
@@ -43,7 +43,7 @@ var PARAM_DESCRIPTIONS = {
43
43
  pattern: "AST pattern (ast-grep style), such as console.log($$$ARGS)",
44
44
  patternContext: "Surrounding code context for sub-statement patterns",
45
45
  selector: "Target AST node kind to extract from pattern context",
46
- grepPattern: "Regex pattern; set literal for exact text",
46
+ grepPattern: "Native regex pattern; set literal for exact text. Empty regex matches every line; empty literal is invalid.",
47
47
  name: "Qualified or unqualified symbol name",
48
48
  refsName: "Identifier to find references for",
49
49
  gitScope: "In a Git repository, which files to search: tracked, untracked, ignored (untracked plus ignored), or all. Defaults to tracked plus untracked.",
@@ -60,7 +60,6 @@ var PARAM_DESCRIPTIONS = {
60
60
  kind: "Restrict structure to this node kind",
61
61
  outline: "Show outline of document hierarchy or source symbols",
62
62
  noAnchors: "Omit source line anchors",
63
- text: "Literal native text query; adapter grep also supports regex via pattern",
64
63
  edits: "Use set_line, replace_lines, insert_after, replace_symbol, or replace",
65
64
  editVariant: "Exactly one nested edit variant",
66
65
  setLine: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}',
@@ -92,9 +91,10 @@ var PARAM_DESCRIPTIONS = {
92
91
  literal: "Treat pattern literally",
93
92
  context: "Surrounding lines for each match",
94
93
  matchLimit: "Maximum matches to return",
94
+ grepLimit: "Maximum matching lines to return; positive integer, defaults to 100",
95
95
  summary: "Return per-file counts",
96
96
  grepScope: "Group matches by enclosing symbol",
97
- scopeContext: "Context lines within each symbol"
97
+ scopeContext: "Non-negative context lines within each symbol; requires scope: symbol"
98
98
  };
99
99
  var TOOL_DESCRIPTIONS = {
100
100
  show: "Read source, document content, or image descriptors without inference. Obtain fresh `LINE:HASH` anchors before edits; use at with a column for cursor context. Document results include typed nodes and cached asset paths.",
@@ -107,7 +107,7 @@ var TOOL_DESCRIPTIONS = {
107
107
  objects: "Explicitly infer object labels and bounding boxes using an installed model.",
108
108
  ocr: "Explicitly recognize text from pixels using an installed model; reading never silently invokes OCR.",
109
109
  search: "Find AST nodes by pattern or KDL selection rule. Returns edit-ready anchors. Use `grep` for text and regex.",
110
- grep: "Search text or regex and return edit-ready `LINE:HASH` anchors. Native CLI text is literal; adapters preserve regex, glob, context, summary, and symbol grouping.",
110
+ grep: "Search with native regex or literal matching. Returns matching-line counts, source LINE:HASH anchors, and document node-local locations. Native glob, context, summary, limits, Git scope, and symbol grouping apply; default limit is 100 matching lines.",
111
111
  def: "Find a symbol definition or declaration by qualified or unqualified name.",
112
112
  refs: "Find identifier-name matches with name, or binding-scoped references with at. Use before renaming or deleting a symbol.",
113
113
  edit: "Edit existing files with fresh `LINE:HASH` anchors. Each edits item contains exactly one set_line, replace_lines, insert_after, replace_symbol, or replace object. Preview before applying a verified plan.",
@@ -215,7 +215,7 @@ var TOOL_CONTRACTS = [
215
215
  tool("objects", "Detect objects", { ...path, ...at, ...stdin, ...level }, ["path"]),
216
216
  tool("ocr", "Recognize text", { ...path, ...at, ...stdin, ...level }, ["path"]),
217
217
  tool("search", "Search AST nodes", { ...path, ...lang, ...stdin, ...limit, ...gitScopeSchema, ...noAnchors, pattern: string(PARAM_DESCRIPTIONS.pattern), context: string(PARAM_DESCRIPTIONS.patternContext), selector: string(PARAM_DESCRIPTIONS.selector), rule: string(PARAM_DESCRIPTIONS.rule) }, []),
218
- tool("grep", "Search text", { ...path, ...limit, ...gitScopeSchema, text: string(PARAM_DESCRIPTIONS.text), pattern: string(PARAM_DESCRIPTIONS.grepPattern), glob: string(PARAM_DESCRIPTIONS.glob), ignoreCase: { type: "boolean", description: PARAM_DESCRIPTIONS.ignoreCase }, literal: { type: "boolean", description: PARAM_DESCRIPTIONS.literal }, context: integer(PARAM_DESCRIPTIONS.context, 0), summary: { type: "boolean", description: PARAM_DESCRIPTIONS.summary }, scope: { type: "string", enum: ["symbol"], description: PARAM_DESCRIPTIONS.grepScope }, scopeContext: integer(PARAM_DESCRIPTIONS.scopeContext, 0) }, []),
218
+ tool("grep", "Search text", { path: string(PARAM_DESCRIPTIONS.searchPath), limit: { ...integer(PARAM_DESCRIPTIONS.grepLimit), default: 100 }, ...gitScopeSchema, pattern: string(PARAM_DESCRIPTIONS.grepPattern), glob: string(PARAM_DESCRIPTIONS.glob), ignoreCase: { type: "boolean", description: PARAM_DESCRIPTIONS.ignoreCase }, literal: { type: "boolean", description: PARAM_DESCRIPTIONS.literal }, context: integer(PARAM_DESCRIPTIONS.context, 0), summary: { type: "boolean", description: PARAM_DESCRIPTIONS.summary }, scope: { type: "string", enum: ["symbol"], description: PARAM_DESCRIPTIONS.grepScope }, scopeContext: integer(PARAM_DESCRIPTIONS.scopeContext, 0) }, ["pattern"]),
219
219
  tool("def", "Find definitions", { ...path, ...lang, ...gitScopeSchema, ...noAnchors, name: string(PARAM_DESCRIPTIONS.name) }, ["name"]),
220
220
  tool("refs", "Find references", { ...path, ...at, ...lang, ...gitScopeSchema, ...noAnchors, name: string(PARAM_DESCRIPTIONS.refsName) }, []),
221
221
  tool("edit", "Hash-verified edit", { ...path, ...lang, ...gitScopeSchema, ...apply, edits: { type: "array", minItems: 1, items: editItemSchema, description: PARAM_DESCRIPTIONS.edits }, postEditVerify: { type: "boolean", description: PARAM_DESCRIPTIONS.postEditVerify } }, ["path", "edits"]),
@@ -243,7 +243,7 @@ var REPLACEABLE_TOOLS = [
243
243
  {
244
244
  tool: "grep",
245
245
  builtIn: "grep",
246
- benefit: "it returns LINE:HASH anchors."
246
+ benefit: "it runs native regex/literal matching and returns source LINE:HASH anchors or document locations."
247
247
  },
248
248
  {
249
249
  tool: "write",
@@ -325,6 +325,59 @@ function validateReferencesSelection(input) {
325
325
  return { ok: false, error: "exactly one of name or at is required" };
326
326
  return { ok: true };
327
327
  }
328
+ function validateGrepParams(input) {
329
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
330
+ return { ok: false, error: "grep params must be an object" };
331
+ }
332
+ const params = input;
333
+ const allowed = [
334
+ "pattern",
335
+ "path",
336
+ "glob",
337
+ "ignoreCase",
338
+ "literal",
339
+ "context",
340
+ "limit",
341
+ "summary",
342
+ "scope",
343
+ "scopeContext",
344
+ "gitScope",
345
+ "signal",
346
+ "stdin",
347
+ "timeoutMs",
348
+ "cancelable"
349
+ ];
350
+ if (Object.keys(params).some((key) => !allowed.includes(key)))
351
+ return { ok: false, error: "unknown grep parameter" };
352
+ if (typeof params.pattern !== "string")
353
+ return { ok: false, error: "grep requires a pattern string" };
354
+ if (params.literal === true && params.pattern.length === 0)
355
+ return { ok: false, error: "literal pattern must not be empty" };
356
+ for (const key of ["path", "glob"]) {
357
+ if (params[key] !== undefined && (typeof params[key] !== "string" || params[key].length === 0)) {
358
+ return { ok: false, error: `${key} must be a non-empty string` };
359
+ }
360
+ }
361
+ for (const key of ["ignoreCase", "literal", "summary"]) {
362
+ if (params[key] !== undefined && typeof params[key] !== "boolean")
363
+ return { ok: false, error: `${key} must be a boolean` };
364
+ }
365
+ for (const key of ["context", "limit", "scopeContext"]) {
366
+ const value = params[key];
367
+ const minimum = key === "limit" ? 1 : 0;
368
+ if (value !== undefined && (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum)) {
369
+ return { ok: false, error: `${key} must be a ${minimum === 1 ? "positive" : "non-negative"} integer` };
370
+ }
371
+ }
372
+ if (params.scope !== undefined && params.scope !== "symbol")
373
+ return { ok: false, error: "scope must be symbol" };
374
+ if (params.scopeContext !== undefined && params.scope !== "symbol")
375
+ return { ok: false, error: "scopeContext requires scope" };
376
+ if (params.gitScope !== undefined && !GIT_SCOPES.includes(params.gitScope)) {
377
+ return { ok: false, error: "invalid gitScope" };
378
+ }
379
+ return { ok: true };
380
+ }
328
381
  var VARIANT_FIELDS = {
329
382
  set_line: ["anchor", "new_text"],
330
383
  replace_lines: ["start_anchor", "end_anchor", "new_text"],
@@ -423,7 +476,6 @@ function coerceObviousBase10Int(value, name) {
423
476
  return invalidInteger(value, name);
424
477
  }
425
478
  // ../readseek-api/src/argv.ts
426
- var DEFAULT_STRUCTURE_DEPTH = 1;
427
479
  function appendGitScopeArgs(args, input) {
428
480
  if (input.gitScope !== undefined)
429
481
  args.push("--git-scope", input.gitScope);
@@ -684,11 +736,29 @@ function buildSearchArgs(input) {
684
736
  return finish(args);
685
737
  }
686
738
  function buildGrepArgs(input) {
687
- const args = start("grep", input);
688
- args.push("--text", input.text);
689
- limit2(args, input.limit);
739
+ const validated = validateGrepParams(input);
740
+ if (!validated.ok)
741
+ throw new Error(validated.error);
742
+ const args = ["grep", "--kdl", `--pattern=${input.pattern}`];
743
+ const needsPathTerminator = input.path !== undefined && input.path.startsWith("-") && input.path !== "-";
744
+ if (input.path !== undefined && !needsPathTerminator)
745
+ args.push(input.path);
746
+ if (input.literal)
747
+ args.push("--literal");
748
+ if (input.ignoreCase)
749
+ args.push("--ignore-case");
750
+ if (input.glob !== undefined)
751
+ args.push(`--glob=${input.glob}`);
752
+ pushOptionalNumber(args, "--context", input.context);
753
+ limit2(args, input.limit ?? 100);
754
+ if (input.summary)
755
+ args.push("--summary");
756
+ pushOptionalString(args, "--scope", input.scope);
757
+ pushOptionalNumber(args, "--scope-context", input.scopeContext);
690
758
  appendGitScopeArgs(args, input);
691
- return finish(args);
759
+ if (input.path !== undefined && needsPathTerminator)
760
+ args.push("--", input.path);
761
+ return args;
692
762
  }
693
763
  function buildDefArgs(input) {
694
764
  const args = start("def", input);
@@ -1120,25 +1190,41 @@ var DocumentOutput = Type.Object({
1120
1190
  assets: Type.Array(DocumentAsset, { default: [] }),
1121
1191
  vision: Type.Array(DocumentVision, { default: [] })
1122
1192
  });
1123
- var TextHit = Type.Object({
1124
- line: Int,
1125
- column: Int,
1193
+ var GrepCount = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
1194
+ var GrepPosition = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
1195
+ var GrepAnchor = Type.String({ pattern: "^[1-9][0-9]*:[0-9a-f]{6}$" });
1196
+ var GrepLineSchema = Type.Object({
1197
+ line: GrepPosition,
1198
+ hash: Type.String({ pattern: "^[0-9a-f]{6}$" }),
1126
1199
  text: Str,
1127
- start_line: Int,
1128
- end_line: Int,
1129
- start_byte: Int,
1130
- end_byte: Int,
1131
- hashlines: Hashlines
1200
+ kind: Type.Union([Type.Literal("match"), Type.Literal("context")])
1132
1201
  });
1133
- var DocumentHit = Type.Object({ node_id: Str, kind: Str, snippet: Str, page: OptInt });
1134
- var TextSearchOutput = Type.Object({
1135
- text: Str,
1136
- mode: Type.Literal("literal"),
1137
- total_matches: Int,
1138
- returned_matches: Int,
1202
+ var GrepHitSchema = Type.Object({ line: GrepPosition, column: GrepPosition, start_byte: GrepCount, end_byte: GrepCount, text: Str, anchor: GrepAnchor });
1203
+ var GrepSymbolSchema = Type.Object({ name: Str, kind: Str, start_line: GrepPosition, end_line: GrepPosition, lines: Type.Array(GrepLineSchema) });
1204
+ var GrepSourceResultSchema = Type.Object({
1205
+ file: Str,
1206
+ file_hash: Str,
1207
+ language: Str,
1208
+ total_matches: GrepCount,
1209
+ returned_matches: GrepCount,
1210
+ hits: Type.Array(GrepHitSchema),
1211
+ lines: Type.Array(GrepLineSchema),
1212
+ symbols: Type.Array(GrepSymbolSchema)
1213
+ });
1214
+ var GrepDocumentHitSchema = Type.Object({ node_id: Str, kind: Str, snippet: Str, line: GrepPosition, page: Type.Optional(GrepPosition) });
1215
+ var GrepDocumentResultSchema = Type.Object({ file: Str, content_hash: Str, total_matches: GrepCount, returned_matches: GrepCount, hits: Type.Array(GrepDocumentHitSchema) });
1216
+ var GrepOutputSchema = Type.Object({
1217
+ pattern: Str,
1218
+ mode: Type.Union([Type.Literal("regex"), Type.Literal("literal")]),
1219
+ ignore_case: Bool,
1220
+ summary: Bool,
1221
+ total_matches: GrepCount,
1222
+ returned_matches: GrepCount,
1139
1223
  truncated: Bool,
1140
- results: Type.Array(Type.Object({ file: Str, file_hash: Str, matches: Type.Array(TextHit) })),
1141
- documents: Type.Array(Type.Object({ file: Str, content_hash: Str, hits: Type.Array(DocumentHit) }))
1224
+ count_unit: Type.Literal("lines"),
1225
+ scope: Type.Optional(Type.Literal("symbol")),
1226
+ results: Type.Array(GrepSourceResultSchema),
1227
+ documents: Type.Array(GrepDocumentResultSchema)
1142
1228
  });
1143
1229
  var RuleFinding = Type.Object({
1144
1230
  name: Str,
@@ -1183,8 +1269,35 @@ function parseStructureEnvelope(value) {
1183
1269
  throw new Error("invalid readseek structure: missing structure");
1184
1270
  return { ...output.metadata !== undefined ? { metadata: parseDetectOutput(output.metadata) } : {}, structure: parseStructureOutput(output.structure) };
1185
1271
  }
1186
- function parseTextSearchOutput(value) {
1187
- return parse(TextSearchOutput, value, "text search output");
1272
+ function parseGrepOutput(value) {
1273
+ const output = parse(GrepOutputSchema, value, "grep output");
1274
+ const invalid = (message) => {
1275
+ throw new Error(`invalid readseek grep output: ${message}`);
1276
+ };
1277
+ const groups = [...output.results, ...output.documents];
1278
+ if (output.returned_matches > output.total_matches || groups.reduce((sum, group) => sum + group.total_matches, 0) !== output.total_matches || groups.reduce((sum, group) => sum + group.returned_matches, 0) !== output.returned_matches)
1279
+ invalid("inconsistent counts");
1280
+ for (const group of groups) {
1281
+ if (group.total_matches === 0 || group.returned_matches > group.total_matches)
1282
+ invalid("inconsistent file counts");
1283
+ if (output.summary ? group.hits.length !== 0 : group.hits.length !== group.returned_matches)
1284
+ invalid("inconsistent hit count");
1285
+ }
1286
+ for (const result of output.results) {
1287
+ if (output.summary && (result.lines.length > 0 || result.symbols.length > 0))
1288
+ invalid("summary contains display entries");
1289
+ if (output.scope !== "symbol" && result.symbols.length > 0)
1290
+ invalid("symbols require symbol scope");
1291
+ for (const hit of result.hits) {
1292
+ if (hit.end_byte < hit.start_byte || Number(hit.anchor.split(":")[0]) !== hit.line)
1293
+ invalid("invalid hit span or anchor");
1294
+ }
1295
+ for (const symbol of result.symbols) {
1296
+ if (symbol.end_line < symbol.start_line || symbol.lines.some((line) => line.line < symbol.start_line || line.line > symbol.end_line))
1297
+ invalid("invalid symbol range");
1298
+ }
1299
+ }
1300
+ return output;
1188
1301
  }
1189
1302
  function parseRulesOutput(value) {
1190
1303
  return parse(RulesOutput, value, "rules output");
@@ -1301,9 +1414,9 @@ function createReadSeekClient(runner) {
1301
1414
  const value = await runner.kdl(buildSearchArgs({ ...options, path }), runOptions(options));
1302
1415
  return parseSearchOutput(value);
1303
1416
  },
1304
- async grep(path, options) {
1305
- const value = await runner.kdl(buildGrepArgs({ ...options, path }), runOptions(options));
1306
- return parseTextSearchOutput(value);
1417
+ async grep(params) {
1418
+ const value = await runner.kdl(buildGrepArgs(params), runOptions(params));
1419
+ return parseGrepOutput(value);
1307
1420
  },
1308
1421
  async def(path, options) {
1309
1422
  const value = await runner.kdl(buildDefArgs({ ...options, path }), runOptions(options));
@@ -1786,6 +1899,46 @@ function search(root) {
1786
1899
  }))
1787
1900
  };
1788
1901
  }
1902
+ function grepChildren(node, allowed) {
1903
+ if (node.children.some((child) => !allowed.includes(child.name))) {
1904
+ throw new Error(`invalid readseek KDL grep: unexpected child of ${node.name}`);
1905
+ }
1906
+ }
1907
+ function grepLine(node) {
1908
+ grepChildren(node, []);
1909
+ const parsed = line(node);
1910
+ if (requireProperty(node, "number") !== parsed.line)
1911
+ throw new Error("invalid readseek KDL grep line number");
1912
+ return { ...parsed, kind: requireProperty(node, "kind") };
1913
+ }
1914
+ function grep(root) {
1915
+ grepChildren(root, ["match", "document"]);
1916
+ return parseGrepOutput({
1917
+ ...properties(root),
1918
+ results: children(root, "match").map((match) => {
1919
+ grepChildren(match, ["hit", "line", "symbol"]);
1920
+ return {
1921
+ ...properties(match),
1922
+ hits: children(match, "hit").map((hit) => {
1923
+ grepChildren(hit, []);
1924
+ return properties(hit);
1925
+ }),
1926
+ lines: children(match, "line").map(grepLine),
1927
+ symbols: children(match, "symbol").map((symbol) => {
1928
+ grepChildren(symbol, ["line"]);
1929
+ return { ...properties(symbol), lines: children(symbol, "line").map(grepLine) };
1930
+ })
1931
+ };
1932
+ }),
1933
+ documents: children(root, "document").map((document) => {
1934
+ grepChildren(document, ["hit"]);
1935
+ return { ...properties(document), hits: children(document, "hit").map((hit) => {
1936
+ grepChildren(hit, []);
1937
+ return properties(hit);
1938
+ }) };
1939
+ })
1940
+ });
1941
+ }
1789
1942
  function references(root) {
1790
1943
  const parsed = children(root, "reference").map((reference) => {
1791
1944
  const sourceLine = child(reference, "line");
@@ -1901,7 +2054,7 @@ function parseReadSeekKdl(text) {
1901
2054
  case "document":
1902
2055
  return root.properties.has("format") ? pdfDocument(root) : sourceDocument(root);
1903
2056
  case "search":
1904
- return search(root);
2057
+ return ["mode", "ignore_case", "summary", "count_unit"].some((key) => root.properties.has(key)) ? grep(root) : search(root);
1905
2058
  case "check":
1906
2059
  return diagnostics(root);
1907
2060
  case "refs":
@@ -2975,19 +3128,13 @@ var readSeekCaption = client2.caption;
2975
3128
  var readSeekObjects = client2.objects;
2976
3129
  var readSeekOcr = client2.ocr;
2977
3130
  var readSeekSearch = client2.search;
3131
+ var readSeekGrep = client2.grep;
2978
3132
  var readSeekRefs = client2.refs;
2979
3133
  var readSeekDef = client2.def;
2980
3134
  var readSeekSyntax = client2.syntax;
2981
3135
  var readSeekCheck = client2.check;
2982
3136
  var readSeekEdit = client2.edit;
2983
3137
  var readSeekRename = client2.rename;
2984
- async function readSeekMap(filePath, options = {}) {
2985
- const envelope = await client2.tree(filePath, { depth: DEFAULT_STRUCTURE_DEPTH, signal: options.signal });
2986
- const map = "structure" in envelope ? envelope.structure : undefined;
2987
- if (!map || map.language === "unknown" && map.symbols.length === 0)
2988
- return null;
2989
- return map;
2990
- }
2991
3138
 
2992
3139
  // src/edit-syntax-validate.ts
2993
3140
  function dedupeSortLines(diagnostics) {
@@ -3907,29 +4054,12 @@ ${theme.fg("error", info.errorText)}`;
3907
4054
  }
3908
4055
 
3909
4056
  // src/grep.ts
3910
- import { readFile as fsReadFile2, stat as fsStat } from "node:fs/promises";
3911
- import path5 from "node:path";
3912
- import { createGrepTool } from "@earendil-works/pi-coding-agent";
3913
4057
  import { Type as Type5 } from "@sinclair/typebox";
3914
4058
  import { Text as Text4 } from "@earendil-works/pi-tui";
3915
4059
 
3916
- // src/binary-detect.ts
3917
- function looksLikeBinary(buf) {
3918
- if (buf.length === 0)
3919
- return false;
3920
- if (buf.includes(0))
3921
- return true;
3922
- const decoded = buf.toString("utf8");
3923
- if (!decoded.includes("�"))
3924
- return false;
3925
- return !Buffer.from(decoded, "utf8").equals(buf);
3926
- }
3927
-
3928
4060
  // src/grep-output.ts
3929
- import {
3930
- formatSize,
3931
- truncateHead
3932
- } from "@earendil-works/pi-coding-agent";
4061
+ import path5 from "node:path";
4062
+ import { formatSize, truncateHead } from "@earendil-works/pi-coding-agent";
3933
4063
 
3934
4064
  // src/grep-budget.ts
3935
4065
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
@@ -3949,283 +4079,75 @@ function resolveGrepOutputBudget() {
3949
4079
  }
3950
4080
 
3951
4081
  // src/grep-output.ts
3952
- function hasScope(group) {
3953
- return group.scope !== undefined;
3954
- }
3955
- function renderEntry(displayPath, entry) {
3956
- if (entry.kind === "separator")
3957
- return entry.text;
3958
- const marker = entry.kind === "match" ? ">>" : " ";
3959
- return `${displayPath}:${marker}${entry.line.anchor}|${entry.line.display}`;
3960
- }
3961
- function renderGroupHeader(group) {
3962
- if (!group.scope) {
3963
- return `--- ${group.displayPath} (${group.matchCount} matches) ---`;
3964
- }
3965
- const parent = group.scope.symbol.parentName ? ` in ${group.scope.symbol.parentName}` : "";
3966
- const suffix = group.scope.contextLines !== undefined ? `, scoped to ±${group.scope.contextLines} lines` : "";
3967
- return `--- ${group.displayPath} :: ${group.scope.symbol.kind} ${group.scope.symbol.name}${parent} (${group.scope.symbol.startLine}-${group.scope.symbol.endLine}, ${group.matchCount} matches${suffix}) ---`;
3968
- }
3969
- function buildScopeMetadata(groups, warnings) {
3970
- return {
3971
- mode: "symbol",
3972
- groups: groups.filter(hasScope).map((group) => ({
3973
- path: group.absolutePath,
3974
- displayPath: group.displayPath,
3975
- symbol: group.scope.symbol,
3976
- matchCount: group.matchCount,
3977
- matchLines: [...group.scope.matchLines],
3978
- lineAnchors: group.entries.flatMap((entry) => entry.kind === "separator" ? [] : [entry.line.anchor])
3979
- })),
3980
- warnings: [...warnings]
3981
- };
3982
- }
3983
- function buildGrepOutput(input) {
3984
- const fileCount = new Set(input.groups.map((group) => group.absolutePath)).size;
3985
- const header = `[${input.totalMatches} matches in ${fileCount} files]`;
3986
- let text;
3987
- if (input.summary) {
3988
- const fileLines = [...input.groups].sort((a, b) => b.matchCount - a.matchCount).map((group) => `${group.absolutePath}: ${group.matchCount} matches`);
3989
- text = [header, ...fileLines].join(`
3990
- `);
3991
- } else {
3992
- const blocks = [header];
3993
- for (const group of input.groups) {
3994
- blocks.push(renderGroupHeader(group));
3995
- for (const entry of group.entries) {
3996
- blocks.push(renderEntry(group.displayPath, entry));
4082
+ function buildGrepOutput(output, cwd) {
4083
+ const recordBlocks = new Map;
4084
+ const fileCount = new Set([...output.results, ...output.documents].map((group) => path5.resolve(cwd, group.file))).size;
4085
+ const blocks = [`[${output.total_matches} matches in ${fileCount} files]`];
4086
+ for (const group of output.results) {
4087
+ const absolutePath = path5.resolve(cwd, group.file);
4088
+ const displayPath = path5.relative(cwd, absolutePath) || absolutePath;
4089
+ if (output.summary) {
4090
+ blocks.push(`${absolutePath}: ${group.total_matches} matches`);
4091
+ continue;
4092
+ }
4093
+ blocks.push(`--- ${displayPath} (${group.total_matches} matches) ---`);
4094
+ const renderLines = (lines) => {
4095
+ for (const line of lines) {
4096
+ const built = buildReadSeekLineWithHash(line.line, line.hash, line.text);
4097
+ const marker = line.kind === "match" ? ">>" : " ";
4098
+ blocks.push(`${displayPath}:${marker}${built.anchor}|${built.display}`);
4099
+ recordBlocks.set(blocks.length - 1, { path: absolutePath, line: line.line, anchor: built.anchor, kind: line.kind });
3997
4100
  }
4101
+ };
4102
+ renderLines(group.lines);
4103
+ for (const symbol of group.symbols) {
4104
+ blocks.push(`--- ${displayPath} :: ${symbol.kind} ${symbol.name} (${symbol.start_line}-${symbol.end_line}) ---`);
4105
+ renderLines(symbol.lines);
3998
4106
  }
3999
- text = blocks.join(`
4000
- `);
4001
4107
  }
4002
- const passthroughLines = input.passthroughLines ?? [];
4003
- if (passthroughLines.length > 0) {
4004
- text += `
4005
-
4006
- ${passthroughLines.join(`
4007
- `)}`;
4008
- }
4009
- if (input.limit !== undefined && input.totalMatches === input.limit) {
4010
- text += `
4011
-
4012
- [Results truncated at ${input.limit} matches — refine pattern or increase limit]`;
4108
+ for (const group of output.documents) {
4109
+ const absolutePath = path5.resolve(cwd, group.file);
4110
+ const displayPath = path5.relative(cwd, absolutePath) || absolutePath;
4111
+ if (output.summary) {
4112
+ blocks.push(`${absolutePath}: ${group.total_matches} matches`);
4113
+ continue;
4114
+ }
4115
+ blocks.push(`--- ${displayPath} (${group.total_matches} matches, document) ---`);
4116
+ for (const hit of group.hits) {
4117
+ const page = hit.page === undefined ? "" : ` page:${hit.page}`;
4118
+ blocks.push(`${displayPath} node:${hit.node_id}${page} (${hit.kind}): ${escapeControlCharsForDisplay(hit.snippet)}`);
4119
+ }
4013
4120
  }
4014
- const scopeWarnings = input.scopeWarnings ?? [];
4015
- if (!input.summary && input.scopeMode === "symbol" && scopeWarnings.length > 0) {
4016
- text = `${scopeWarnings.map((warning) => warning.message).join(`
4017
-
4018
- `)}
4019
-
4020
- ${text}`;
4121
+ if (output.truncated) {
4122
+ blocks.push(`
4123
+ [Results truncated: ${output.returned_matches} of ${output.total_matches} matching lines returned — refine pattern or increase limit]`);
4021
4124
  }
4125
+ let text = blocks.join(`
4126
+ `);
4022
4127
  const budget = resolveGrepOutputBudget();
4023
- const truncated = truncateHead(text, {
4024
- maxLines: budget.maxLines,
4025
- maxBytes: budget.maxBytes
4128
+ const truncated = truncateHead(text, { maxLines: budget.maxLines, maxBytes: budget.maxBytes });
4129
+ let endOffset = 0;
4130
+ const records = blocks.flatMap((block, index) => {
4131
+ endOffset += block.length + (index > 0 ? 1 : 0);
4132
+ const record = recordBlocks.get(index);
4133
+ return record && endOffset <= truncated.content.length ? [record] : [];
4026
4134
  });
4027
4135
  if (truncated.truncated) {
4028
4136
  text = `${truncated.content}
4029
4137
 
4030
4138
  [Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
4031
4139
  }
4032
- const readSeekValue = {
4033
- tool: "grep",
4034
- summary: input.summary,
4035
- totalMatches: input.totalMatches,
4036
- records: input.records.map((record) => ({
4037
- path: record.path,
4038
- line: record.line,
4039
- anchor: record.anchor,
4040
- kind: record.kind
4041
- }))
4042
- };
4043
- if (!input.summary && input.scopeMode === "symbol") {
4044
- readSeekValue.scopes = buildScopeMetadata(input.groups, scopeWarnings);
4045
- }
4046
4140
  return {
4047
4141
  text,
4048
- readSeekValue
4049
- };
4050
- }
4051
-
4052
- // src/file-map.ts
4053
- async function getOrGenerateMap(absPath) {
4054
- try {
4055
- return await readSeekMap(absPath);
4056
- } catch {
4057
- return null;
4058
- }
4059
- }
4060
-
4061
- // src/grep-symbol-scope.ts
4062
- function findEnclosingSymbol(map, lineNumber) {
4063
- const byQualifiedName = new Map;
4064
- for (const symbol of map.symbols) {
4065
- const qname = symbol.qualified_name ?? symbol.name;
4066
- if (!byQualifiedName.has(qname))
4067
- byQualifiedName.set(qname, symbol);
4068
- }
4069
- const candidates = map.symbols.filter((symbol) => lineNumber >= symbol.start_line && lineNumber <= symbol.end_line).map((symbol) => {
4070
- const qname = symbol.qualified_name ?? symbol.name;
4071
- const dot = qname.lastIndexOf(".");
4072
- const parent = dot === -1 ? undefined : byQualifiedName.get(qname.slice(0, dot));
4073
- return {
4074
- name: symbol.name,
4075
- kind: symbol.kind === "constructor" ? "method" : symbol.kind,
4076
- startLine: symbol.start_line,
4077
- endLine: symbol.end_line,
4078
- parentName: parent?.name
4079
- };
4080
- }).sort((a, b) => {
4081
- const rangeA = a.endLine - a.startLine;
4082
- const rangeB = b.endLine - b.startLine;
4083
- if (rangeA !== rangeB)
4084
- return rangeA - rangeB;
4085
- if (a.startLine !== b.startLine)
4086
- return a.startLine - b.startLine;
4087
- return a.name.localeCompare(b.name);
4088
- });
4089
- return candidates[0] ?? null;
4090
- }
4091
- function firstLineNumber(group) {
4092
- const first = group.entries.find((e) => e.kind !== "separator");
4093
- return first ? first.line.line : Number.MAX_SAFE_INTEGER;
4094
- }
4095
- function buildSymbolEntries(fileLines, symbol, matchLines, scopeContext) {
4096
- if (scopeContext === undefined) {
4097
- const entries = [];
4098
- for (let lineNumber = symbol.startLine;lineNumber <= symbol.endLine; lineNumber++) {
4099
- const built = buildReadSeekLine(lineNumber, fileLines[lineNumber - 1] ?? "");
4100
- entries.push({ kind: matchLines.has(lineNumber) ? "match" : "context", line: built });
4101
- }
4102
- return entries;
4103
- }
4104
- const ranges = [...matchLines].sort((a, b) => a - b).map((ln) => ({
4105
- startLine: Math.max(symbol.startLine, ln - scopeContext),
4106
- endLine: Math.min(symbol.endLine, ln + scopeContext)
4107
- }));
4108
- const merged = [];
4109
- for (const r of ranges) {
4110
- const last = merged[merged.length - 1];
4111
- if (last && r.startLine <= last.endLine + 1) {
4112
- last.endLine = Math.max(last.endLine, r.endLine);
4113
- } else {
4114
- merged.push({ ...r });
4115
- }
4116
- }
4117
- const entries = [];
4118
- for (let i = 0;i < merged.length; i++) {
4119
- if (i > 0)
4120
- entries.push({ kind: "separator", text: "--" });
4121
- const range = merged[i];
4122
- for (let ln = range.startLine;ln <= range.endLine; ln++) {
4123
- const built = buildReadSeekLine(ln, fileLines[ln - 1] ?? "");
4124
- entries.push({ kind: matchLines.has(ln) ? "match" : "context", line: built });
4125
- }
4126
- }
4127
- return entries;
4128
- }
4129
- function buildFallbackEntries(fileLines, matchLines, contextLines) {
4130
- const lineMap = new Map;
4131
- for (const matchLine of matchLines) {
4132
- const start = Math.max(1, matchLine - contextLines);
4133
- const end = Math.min(fileLines.length, matchLine + contextLines);
4134
- for (let lineNumber = start;lineNumber <= end; lineNumber++) {
4135
- const built = buildReadSeekLine(lineNumber, fileLines[lineNumber - 1] ?? "");
4136
- const candidate = { kind: lineNumber === matchLine ? "match" : "context", line: built };
4137
- const existing = lineMap.get(lineNumber);
4138
- if (!existing || existing.kind === "context" && candidate.kind === "match") {
4139
- lineMap.set(lineNumber, candidate);
4140
- }
4141
- }
4142
- }
4143
- const ordered = [...lineMap.entries()].sort(([a], [b]) => a - b);
4144
- const entries = [];
4145
- for (let i = 0;i < ordered.length; i++) {
4146
- if (i > 0 && ordered[i][0] > ordered[i - 1][0] + 1)
4147
- entries.push({ kind: "separator", text: "--" });
4148
- entries.push(ordered[i][1]);
4149
- }
4150
- return entries;
4151
- }
4152
- function scopeGrepGroupsToSymbols(input) {
4153
- const warnings = [];
4154
- const rendered = [];
4155
- for (const group of input.groups) {
4156
- const fileLines = input.fileLinesByPath.get(group.absolutePath);
4157
- const fileMap = input.fileMapsByPath.get(group.absolutePath) ?? null;
4158
- if (!fileLines || !fileMap) {
4159
- warnings.push({
4160
- code: "unmappable-file",
4161
- message: `[Warning: symbol scoping unavailable for ${group.absolutePath} — showing normal grep lines for this file]`,
4162
- path: group.absolutePath
4163
- });
4164
- rendered.push({ order: firstLineNumber(group), group });
4165
- continue;
4166
- }
4167
- const symbolBuckets = new Map;
4168
- const fallbackMatchLines = [];
4169
- for (const entry of group.entries) {
4170
- if (entry.kind !== "match")
4171
- continue;
4172
- const symbol = findEnclosingSymbol(fileMap, entry.line.line);
4173
- if (!symbol) {
4174
- fallbackMatchLines.push(entry.line.line);
4175
- warnings.push({
4176
- code: "no-enclosing-symbol",
4177
- message: `[Warning: no enclosing symbol for ${group.absolutePath}:${entry.line.line} — showing normal grep lines for this match]`,
4178
- path: group.absolutePath,
4179
- line: entry.line.line
4180
- });
4181
- continue;
4182
- }
4183
- const key = `${symbol.startLine}:${symbol.endLine}:${symbol.parentName ?? ""}:${symbol.name}`;
4184
- const bucket = symbolBuckets.get(key) ?? { symbol, matchLines: new Set };
4185
- bucket.matchLines.add(entry.line.line);
4186
- symbolBuckets.set(key, bucket);
4187
- }
4188
- const scopedGroups = [...symbolBuckets.values()].sort((a, b) => {
4189
- if (a.symbol.startLine !== b.symbol.startLine)
4190
- return a.symbol.startLine - b.symbol.startLine;
4191
- return a.symbol.name.localeCompare(b.symbol.name);
4192
- }).map(({ symbol, matchLines }) => ({
4193
- displayPath: group.displayPath,
4194
- absolutePath: group.absolutePath,
4195
- matchCount: matchLines.size,
4196
- scope: {
4197
- mode: "symbol",
4198
- symbol,
4199
- matchLines: [...matchLines].sort((a, b) => a - b),
4200
- ...input.scopeContext !== undefined ? { contextLines: input.scopeContext } : {}
4201
- },
4202
- entries: buildSymbolEntries(fileLines, symbol, matchLines, input.scopeContext)
4203
- }));
4204
- for (const scopedGroup of scopedGroups)
4205
- rendered.push({ order: scopedGroup.scope.symbol.startLine, group: scopedGroup });
4206
- if (fallbackMatchLines.length > 0) {
4207
- rendered.push({
4208
- order: Math.min(...fallbackMatchLines),
4209
- group: {
4210
- displayPath: group.displayPath,
4211
- absolutePath: group.absolutePath,
4212
- matchCount: fallbackMatchLines.length,
4213
- entries: buildFallbackEntries(fileLines, fallbackMatchLines, input.contextLines)
4214
- }
4215
- });
4216
- }
4217
- if (scopedGroups.length === 0 && fallbackMatchLines.length === 0) {
4218
- rendered.push({ order: firstLineNumber(group), group });
4142
+ readSeekValue: {
4143
+ ...output,
4144
+ tool: "grep",
4145
+ totalMatches: output.total_matches,
4146
+ returnedMatches: output.returned_matches,
4147
+ fileCount,
4148
+ records
4219
4149
  }
4220
- }
4221
- rendered.sort((a, b) => {
4222
- if (a.order !== b.order)
4223
- return a.order - b.order;
4224
- if (a.group.absolutePath !== b.group.absolutePath)
4225
- return a.group.absolutePath.localeCompare(b.group.absolutePath);
4226
- return a.group.displayPath.localeCompare(b.group.displayPath);
4227
- });
4228
- return { groups: rendered.map((item) => item.group), warnings };
4150
+ };
4229
4151
  }
4230
4152
 
4231
4153
  // src/grep-render-helpers.ts
@@ -4243,7 +4165,6 @@ function formatGrepCallText(args) {
4243
4165
  suffix: parts.length > 0 ? parts.join(" ") : undefined
4244
4166
  };
4245
4167
  }
4246
- var GREP_TRUNCATION_THRESHOLD = 50;
4247
4168
  function formatGrepResultText(input) {
4248
4169
  if (input.isError && input.errorText) {
4249
4170
  return {
@@ -4256,17 +4177,15 @@ function formatGrepResultText(input) {
4256
4177
  }
4257
4178
  const { totalMatches, summary, fileCount } = input;
4258
4179
  const noMatches = totalMatches === 0;
4259
- const truncated = totalMatches > GREP_TRUNCATION_THRESHOLD;
4180
+ const truncated = input.truncated ?? false;
4260
4181
  const matchWord = totalMatches === 1 ? "match" : "matches";
4261
4182
  const fileWord = fileCount === 1 ? "file" : "files";
4262
4183
  const summaryText = noMatches ? "" : `✓ ${totalMatches} ${matchWord} in ${fileCount} ${fileWord}`;
4263
4184
  const badges = [];
4264
4185
  if (truncated)
4265
- badges.push("10/file cap");
4186
+ badges.push("truncated");
4266
4187
  if (summary)
4267
4188
  badges.push("summary");
4268
- if (input.hasBinaryWarning)
4269
- badges.push("⚠ binary");
4270
4189
  return {
4271
4190
  summary: summaryText,
4272
4191
  badges,
@@ -4319,78 +4238,12 @@ var grepSchema = Type5.Object({
4319
4238
  scope: Type5.Optional(Type5.Literal("symbol", {
4320
4239
  description: PARAM_DESCRIPTIONS.grepScope
4321
4240
  })),
4322
- scopeContext: optionalIntOrString(PARAM_DESCRIPTIONS.scopeContext)
4241
+ scopeContext: optionalIntOrString(PARAM_DESCRIPTIONS.scopeContext),
4242
+ ...readSeekGitSearchParams()
4323
4243
  });
4324
- var MATCH_LINE_RE = /^(.*?):(\d+): (.*)$/;
4325
- var CONTEXT_LINE_RE = /^(.*?)-(\d+)- (.*)$/;
4326
- function parseGrepOutputLine(line) {
4327
- const match = line.match(MATCH_LINE_RE);
4328
- if (match) {
4329
- return {
4330
- kind: "match",
4331
- displayPath: match[1],
4332
- lineNumber: Number.parseInt(match[2], 10),
4333
- text: match[3]
4334
- };
4335
- }
4336
- const context = line.match(CONTEXT_LINE_RE);
4337
- if (context) {
4338
- return {
4339
- kind: "context",
4340
- displayPath: context[1],
4341
- lineNumber: Number.parseInt(context[2], 10),
4342
- text: context[3]
4343
- };
4344
- }
4345
- return null;
4346
- }
4347
- var GREP_MAX_MATCHES_PER_FILE = 10;
4348
- function dedupeContextEntries(entries) {
4349
- if (entries.length === 0)
4350
- return entries;
4351
- const byLine = new Map;
4352
- for (const entry of entries) {
4353
- if (entry.kind === "separator")
4354
- continue;
4355
- const existing = byLine.get(entry.line.line);
4356
- if (!existing || entry.kind === "match" && existing.kind === "context") {
4357
- byLine.set(entry.line.line, entry);
4358
- }
4359
- }
4360
- const sorted = [...byLine.entries()].sort(([a], [b]) => a - b);
4361
- const result = [];
4362
- for (let i = 0;i < sorted.length; i++) {
4363
- if (i > 0 && sorted[i][0] > sorted[i - 1][0] + 1) {
4364
- result.push({ kind: "separator", text: "--" });
4365
- }
4366
- result.push(sorted[i][1]);
4367
- }
4368
- return result;
4369
- }
4370
- function truncateGroupEntries(group) {
4371
- let matchesSeen = 0;
4372
- let truncatedCount = 0;
4373
- const kept = [];
4374
- for (const entry of group.entries) {
4375
- if (entry.kind === "match") {
4376
- matchesSeen++;
4377
- if (matchesSeen <= GREP_MAX_MATCHES_PER_FILE) {
4378
- kept.push(entry);
4379
- } else {
4380
- truncatedCount++;
4381
- }
4382
- } else if (matchesSeen <= GREP_MAX_MATCHES_PER_FILE) {
4383
- kept.push(entry);
4384
- }
4385
- }
4386
- if (truncatedCount > 0) {
4387
- kept.push({ kind: "separator", text: `... +${truncatedCount} more matches` });
4388
- }
4389
- return { ...group, entries: kept };
4390
- }
4391
4244
  async function executeGrep(opts) {
4392
- const { toolCallId, params, signal, onUpdate, cwd, onFileAnchored } = opts;
4393
- await ensureHashInit();
4245
+ const { params, signal, cwd, onFileAnchored } = opts;
4246
+ signal?.throwIfAborted();
4394
4247
  const rawParams = params;
4395
4248
  const context = coerceObviousBase10Int(rawParams.context, "context");
4396
4249
  if (!context.ok) {
@@ -4418,261 +4271,28 @@ async function executeGrep(opts) {
4418
4271
  const message = `Invalid scopeContext: expected a non-negative integer, received ${scopeContext.value}.`;
4419
4272
  return buildToolErrorResult("grep", "invalid-params-combo", message);
4420
4273
  }
4421
- const p = {
4422
- ...rawParams,
4274
+ const output = await readSeekGrep({
4275
+ pattern: rawParams.pattern,
4276
+ path: resolveToCwd(rawParams.path ?? ".", cwd),
4277
+ glob: rawParams.glob,
4278
+ ignoreCase: rawParams.ignoreCase,
4279
+ literal: rawParams.literal,
4423
4280
  context: context.value,
4424
4281
  limit: limit.value,
4425
- scopeContext: scopeContext.value
4426
- };
4427
- const builtin = createGrepTool(cwd);
4428
- const result = await builtin.execute(toolCallId, {
4429
- ...p,
4430
- context: context.value,
4431
- limit: limit.value
4432
- }, signal, onUpdate);
4433
- const textBlock = result.content?.find((item) => item.type === "text" && ("text" in item) && typeof item.text === "string");
4434
- if (!textBlock?.text)
4435
- return result;
4436
- const { path: rawSearchPath } = p;
4437
- const searchPath = resolveToCwd(rawSearchPath || ".", cwd);
4438
- let searchPathIsDirectory = false;
4439
- try {
4440
- searchPathIsDirectory = (await fsStat(searchPath)).isDirectory();
4441
- } catch {
4442
- searchPathIsDirectory = false;
4443
- }
4444
- if (!searchPathIsDirectory) {
4445
- try {
4446
- const buf = await fsReadFile2(searchPath);
4447
- if (looksLikeBinary(buf)) {
4448
- const warning = `[Warning: '${p.path ?? searchPath}' appears to be a binary file — grep skips binary files by default. Use a hex tool or the read tool to inspect it.]`;
4449
- return {
4450
- ...result,
4451
- content: result.content.map((item) => item === textBlock ? { ...item, text: warning } : item),
4452
- details: {
4453
- ...typeof result.details === "object" && result.details !== null ? result.details : {},
4454
- readSeekValue: {
4455
- tool: "grep",
4456
- summary: !!p.summary,
4457
- totalMatches: 0,
4458
- records: []
4459
- }
4460
- }
4461
- };
4462
- }
4463
- } catch {}
4464
- }
4465
- const fileCache = new Map;
4466
- const bareCRFiles = new Set;
4467
- const getFileLines = async (absolutePath) => {
4468
- throwIfAborted(signal);
4469
- if (fileCache.has(absolutePath))
4470
- return fileCache.get(absolutePath);
4471
- try {
4472
- const rawBuffer = await fsReadFile2(absolutePath);
4473
- if (looksLikeBinary(rawBuffer)) {
4474
- fileCache.set(absolutePath, undefined);
4475
- return;
4476
- }
4477
- const raw = rawBuffer.toString("utf-8");
4478
- if (hasBareCarriageReturn(raw))
4479
- bareCRFiles.add(absolutePath);
4480
- const lines = normalizeToLF(stripBom(raw).text).split(`
4481
- `);
4482
- fileCache.set(absolutePath, lines);
4483
- return lines;
4484
- } catch {
4485
- fileCache.set(absolutePath, undefined);
4486
- return;
4487
- }
4488
- };
4489
- const toAbsolutePath = (displayPath) => {
4490
- if (searchPathIsDirectory)
4491
- return path5.resolve(searchPath, displayPath);
4492
- return searchPath;
4493
- };
4494
- const groupsByPath = new Map;
4495
- const passthroughLines = [];
4496
- let totalMatches = 0;
4497
- let parsedCount = 0;
4498
- let candidateUnparsedCount = 0;
4499
- const candidateLinePattern = /^.+(?::|-)\d+(?::|-)\s/;
4500
- if (!p.summary) {
4501
- const pathsToRead = new Set;
4502
- for (const line of textBlock.text.split(`
4503
- `)) {
4504
- throwIfAborted(signal);
4505
- const parsed = parseGrepOutputLine(line);
4506
- if (parsed && Number.isFinite(parsed.lineNumber) && parsed.lineNumber >= 1) {
4507
- pathsToRead.add(toAbsolutePath(parsed.displayPath));
4508
- }
4509
- }
4510
- const CONCURRENCY = 8;
4511
- const pathList = [...pathsToRead];
4512
- for (let i = 0;i < pathList.length; i += CONCURRENCY) {
4513
- await Promise.all(pathList.slice(i, i + CONCURRENCY).map((p) => getFileLines(p)));
4514
- }
4515
- }
4516
- const addSummaryMatch = (displayPath, absolutePath) => {
4517
- let group = groupsByPath.get(displayPath);
4518
- if (!group) {
4519
- group = { displayPath, absolutePath, matchCount: 0, entries: [] };
4520
- groupsByPath.set(displayPath, group);
4521
- }
4522
- group.matchCount++;
4523
- totalMatches++;
4524
- };
4525
- const addEntry = (displayPath, absolutePath, kind, line) => {
4526
- let group = groupsByPath.get(displayPath);
4527
- if (!group) {
4528
- group = { displayPath, absolutePath, matchCount: 0, entries: [] };
4529
- groupsByPath.set(displayPath, group);
4530
- }
4531
- group.entries.push({ kind, line });
4532
- if (kind === "match") {
4533
- group.matchCount++;
4534
- totalMatches++;
4535
- }
4536
- };
4537
- for (const line of textBlock.text.split(`
4538
- `)) {
4539
- throwIfAborted(signal);
4540
- const parsed = parseGrepOutputLine(line);
4541
- if (!parsed || !Number.isFinite(parsed.lineNumber) || parsed.lineNumber < 1) {
4542
- if (candidateLinePattern.test(line)) {
4543
- candidateUnparsedCount++;
4544
- }
4545
- const trimmed = line.trim();
4546
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
4547
- passthroughLines.push(trimmed);
4548
- }
4549
- continue;
4550
- }
4551
- parsedCount++;
4552
- const absolute = toAbsolutePath(parsed.displayPath);
4553
- if (p.summary) {
4554
- if (parsed.kind === "match") {
4555
- addSummaryMatch(parsed.displayPath, absolute);
4556
- }
4557
- continue;
4558
- }
4559
- const fileLines = await getFileLines(absolute);
4560
- if (fileLines === undefined)
4561
- continue;
4562
- if (parsed.kind === "match" && bareCRFiles.has(absolute)) {
4563
- const flags = p.ignoreCase ? "i" : "";
4564
- let patternRe = null;
4565
- try {
4566
- patternRe = p.literal ? new RegExp(p.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags) : new RegExp(p.pattern, flags);
4567
- } catch {}
4568
- if (patternRe !== null) {
4569
- let emitted = false;
4570
- for (let i = 0;i < fileLines.length; i++) {
4571
- if (!patternRe.test(fileLines[i]))
4572
- continue;
4573
- addEntry(parsed.displayPath, absolute, "match", buildReadSeekLine(i + 1, fileLines[i]));
4574
- emitted = true;
4575
- }
4576
- if (emitted)
4577
- continue;
4578
- }
4579
- }
4580
- const sourceLine = fileLines[parsed.lineNumber - 1] ?? parsed.text;
4581
- const built = buildReadSeekLine(parsed.lineNumber, sourceLine);
4582
- addEntry(parsed.displayPath, absolute, parsed.kind, {
4583
- ...built,
4584
- display: escapeControlCharsForDisplay(parsed.text)
4585
- });
4586
- }
4587
- if (p.summary && parsedCount === 0 && candidateUnparsedCount > 0) {
4588
- const passthroughDetails = typeof result.details === "object" && result.details !== null ? result.details : {};
4589
- return {
4590
- ...result,
4591
- details: {
4592
- ...passthroughDetails,
4593
- readSeekValue: {
4594
- tool: "grep",
4595
- summary: true,
4596
- totalMatches: 0,
4597
- records: []
4598
- }
4599
- }
4600
- };
4601
- }
4602
- if (parsedCount === 0 && candidateUnparsedCount > 0) {
4603
- const warning = "[hashline grep passthrough] Unparsed grep format; returned original output.";
4604
- const passthroughDetails = typeof result.details === "object" && result.details !== null ? result.details : {};
4605
- return {
4606
- ...result,
4607
- content: result.content.map((item) => item === textBlock ? { ...item, text: `${textBlock.text}
4608
-
4609
- ${warning}` } : item),
4610
- details: {
4611
- ...passthroughDetails,
4612
- hashlinePassthrough: true,
4613
- hashlineWarning: warning,
4614
- readSeekValue: {
4615
- tool: "grep",
4616
- summary: !!p.summary,
4617
- totalMatches: 0,
4618
- records: []
4619
- }
4620
- }
4621
- };
4622
- }
4623
- const summary = !!p.summary;
4624
- const effectiveLimit = typeof p.limit === "number" ? p.limit : 100;
4625
- const groups = [...groupsByPath.values()];
4626
- for (const group of groups) {
4627
- group.entries = dedupeContextEntries(group.entries);
4628
- }
4629
- let renderedGroups = totalMatches > GREP_TRUNCATION_THRESHOLD ? groups.map(truncateGroupEntries) : groups;
4630
- let scopeWarnings = [];
4631
- if (p.scope === "symbol" && !summary) {
4632
- const fileLinesByPath = new Map;
4633
- const fileMapsByPath = new Map;
4634
- for (const group of renderedGroups) {
4635
- const lines = await getFileLines(group.absolutePath);
4636
- if (lines)
4637
- fileLinesByPath.set(group.absolutePath, lines);
4638
- fileMapsByPath.set(group.absolutePath, await getOrGenerateMap(group.absolutePath));
4639
- }
4640
- const scoped = scopeGrepGroupsToSymbols({
4641
- groups: renderedGroups,
4642
- fileLinesByPath,
4643
- fileMapsByPath,
4644
- contextLines: typeof p.context === "number" ? p.context : 0,
4645
- scopeContext: typeof p.scopeContext === "number" ? p.scopeContext : undefined
4646
- });
4647
- renderedGroups = scoped.groups;
4648
- scopeWarnings = scoped.warnings;
4649
- }
4650
- const readSeekRecords = renderedGroups.flatMap((group) => group.entries.flatMap((entry) => entry.kind === "separator" ? [] : [{ path: group.absolutePath, ...entry.line, kind: entry.kind }]));
4651
- const builtOutput = buildGrepOutput({
4652
- summary: !!summary,
4653
- totalMatches,
4654
- groups: renderedGroups,
4655
- limit: effectiveLimit,
4656
- records: readSeekRecords,
4657
- scopeMode: p.scope === "symbol" && !summary ? "symbol" : undefined,
4658
- scopeWarnings,
4659
- passthroughLines
4282
+ summary: rawParams.summary,
4283
+ scope: rawParams.scope,
4284
+ scopeContext: scopeContext.value,
4285
+ gitScope: rawParams.gitScope,
4286
+ signal
4660
4287
  });
4661
- if (!summary && readSeekRecords.length > 0) {
4662
- const anchoredPaths = new Set(readSeekRecords.map((record) => record.path));
4663
- for (const absolutePath of anchoredPaths) {
4664
- onFileAnchored?.(absolutePath);
4665
- }
4288
+ signal?.throwIfAborted();
4289
+ const builtOutput = buildGrepOutput(output, cwd);
4290
+ for (const absolutePath of new Set(builtOutput.readSeekValue.records.map((record) => record.path))) {
4291
+ onFileAnchored?.(absolutePath);
4666
4292
  }
4667
- const existingDetails = typeof result.details === "object" && result.details !== null ? result.details : {};
4668
- const { linesTruncated: _ignoredLinesTruncated, truncation: _ignoredTruncation, ...compactDetails } = existingDetails;
4669
4293
  return {
4670
- ...result,
4671
- content: result.content.map((item) => item === textBlock ? { ...item, text: builtOutput.text } : item),
4672
- details: {
4673
- ...compactDetails,
4674
- readSeekValue: builtOutput.readSeekValue
4675
- }
4294
+ content: [{ type: "text", text: builtOutput.text }],
4295
+ details: { readSeekValue: builtOutput.readSeekValue }
4676
4296
  };
4677
4297
  }
4678
4298
  function registerGrepTool(pi, options = {}) {
@@ -4727,22 +4347,16 @@ function registerGrepTool(pi, options = {}) {
4727
4347
  if (isError || result.isError)
4728
4348
  return renderErrorResult(textContent, { expanded, width, theme });
4729
4349
  const readSeekValue = result.details?.readSeekValue;
4730
- const hasBinaryWarning = textContent.includes("appears to be a binary file");
4731
- const fileSet = new Set;
4732
- for (const r of readSeekValue?.records ?? []) {
4733
- if (r.path)
4734
- fileSet.add(r.path);
4735
- }
4736
4350
  const info = formatGrepResultText({
4737
4351
  totalMatches: readSeekValue?.totalMatches ?? 0,
4738
4352
  summary: readSeekValue?.summary ?? false,
4739
4353
  records: readSeekValue?.records ?? [],
4740
- fileCount: fileSet.size,
4741
- hasBinaryWarning
4354
+ fileCount: readSeekValue?.fileCount ?? 0,
4355
+ truncated: readSeekValue?.truncated
4742
4356
  });
4743
- if (info.noMatches && !hasBinaryWarning)
4357
+ if (info.noMatches)
4744
4358
  return new Text4(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
4745
- const matchCount = readSeekValue?.totalMatches ?? 0;
4359
+ const matchCount = readSeekValue?.returnedMatches ?? 0;
4746
4360
  const matchWord = matchCount === 1 ? "match" : "matches";
4747
4361
  let text = summaryLine(`${matchCount} ${matchWord} returned`, {
4748
4362
  hidden: !!textContent && !expanded,
@@ -4769,10 +4383,10 @@ import path6 from "node:path";
4769
4383
  import { Type as Type6 } from "@sinclair/typebox";
4770
4384
 
4771
4385
  // src/stat-search-path.ts
4772
- import { stat as fsStat2 } from "node:fs/promises";
4386
+ import { stat as fsStat } from "node:fs/promises";
4773
4387
  async function statSearchPathOrError(tool, rawPath, searchPath) {
4774
4388
  try {
4775
- return { ok: true, stats: await fsStat2(searchPath) };
4389
+ return { ok: true, stats: await fsStat(searchPath) };
4776
4390
  } catch (err) {
4777
4391
  const path = rawPath ?? searchPath;
4778
4392
  const { code, message } = formatFsError(err, "stat-error");
@@ -5251,6 +4865,18 @@ import { withFileMutationQueue as withFileMutationQueue2, truncateHead as trunca
5251
4865
  import { Text as Text6 } from "@earendil-works/pi-tui";
5252
4866
  import { Type as Type9 } from "@sinclair/typebox";
5253
4867
 
4868
+ // src/binary-detect.ts
4869
+ function looksLikeBinary(buf) {
4870
+ if (buf.length === 0)
4871
+ return false;
4872
+ if (buf.includes(0))
4873
+ return true;
4874
+ const decoded = buf.toString("utf8");
4875
+ if (!decoded.includes("�"))
4876
+ return false;
4877
+ return !Buffer.from(decoded, "utf8").equals(buf);
4878
+ }
4879
+
5254
4880
  // src/pending-diff-preview.ts
5255
4881
  import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync, statSync as statSync4 } from "node:fs";
5256
4882
  import { dirname, isAbsolute, relative as relative2, resolve, sep } from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Pi extension for LINE:HASH-anchored file operations and structural code navigation",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,8 +39,8 @@
39
39
  "node": ">=20.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jarkkojs/readseek": "^0.12.0",
43
- "@jarkkojs/readseek-api": "^0.12.0",
42
+ "@jarkkojs/readseek": "^0.12.1",
43
+ "@jarkkojs/readseek-api": "^0.12.1",
44
44
  "diff": "^9.0.0",
45
45
  "xxhash-wasm": "^1.1.0"
46
46
  },
package/prompts/grep.md CHANGED
@@ -1,4 +1,4 @@
1
- Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, and documentation.
1
+ Search text or regex with native ReadSeek. Source results return edit-ready `LINE:HASH` anchors; document results return `node:`/`page:` selectors, not editable anchors.
2
2
 
3
3
  ## Modes
4
4
 
@@ -13,15 +13,16 @@ Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for
13
13
 
14
14
  ## Parameters
15
15
 
16
- - `pattern` — regular expression by default; set `literal: true` for exact text.
16
+ - `pattern` — UTF-8 RE2 regular expression by default; set `literal: true` for exact text. Lookaround and backreferences are not supported.
17
17
  - `path` — file or directory; defaults to the current directory.
18
18
  - `glob` — file-name filter such as `*.ts` or `**/*.test.ts`.
19
19
  - `ignoreCase` — case-insensitive search.
20
20
  - `context` — surrounding lines in normal mode.
21
- - `limit` — maximum matches; defaults to 100.
21
+ - `limit` — maximum matching lines (not occurrences); positive, defaults to 100.
22
22
  - `summary` — return counts without anchors.
23
23
  - `scope` — only `"symbol"` is supported.
24
- - `scopeContext` — non-negative context inside symbol scope.
24
+ - `scopeContext` — non-negative context inside symbol scope; requires `scope: "symbol"`.
25
+ - `gitScope` — `tracked`, `untracked`, `ignored`, or `all`; defaults to tracked plus untracked files.
25
26
 
26
27
  If `limit` or the display budget truncates output, start with `summary`, narrow
27
28
  the file set and pattern, then request context or symbol blocks.