1 // Meta parser for `grep_search` tool calls. Reads the path/pattern
2 // triplet from args (strict parsing - we wait for the args to
3 // complete) and the matches from the result blob. The result parser
4 // keeps the original "scan result as raw text on JSON.parse failure"
5 // fallback so MCP servers that return unparseable output still get
8 import { BuiltInTool } from '$lib/enums';
9 import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
10 import { parseToolArgs } from './_shared';
12 export type GrepSearchMatch = {
18 export type GrepSearchMeta = {
23 showLineNumbers: boolean;
24 matches: GrepSearchMatch[];
25 totalMatches?: number;
26 errorMessage?: string;
29 export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
30 const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
31 if (!args) return null;
33 const path = typeof args.path === 'string' ? args.path : '';
34 const pattern = typeof args.pattern === 'string' ? args.pattern : '';
35 if (!path || !pattern) return null;
37 const include = typeof args.include === 'string' && args.include ? args.include : '**';
38 const exclude = typeof args.exclude === 'string' && args.exclude ? args.exclude : undefined;
39 const showLineNumbers = args.return_line_numbers === true;
41 let matches: GrepSearchMatch[] = [];
42 let totalMatches: number | undefined;
43 let errorMessage: string | undefined;
45 const toolResultString = section.toolResult;
46 if (toolResultString) {
48 const parsed: unknown = JSON.parse(toolResultString);
49 if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
50 const obj = parsed as Record<string, unknown>;
51 if (typeof obj.error === 'string') {
52 errorMessage = obj.error;
53 } else if (typeof obj.plain_text_response === 'string') {
54 const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
57 matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
61 // Result wasn't JSON: keep behaviour for MCP servers that
62 // emit raw text and treat each line as a `<file>:<content>`
63 // (or `<file>:<line>:<content>`) match.
64 const split = splitSearchSummaryList(toolResultString, (total) => {
67 matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
83 function parseGrepLine(line: string, showLineNumbers: boolean): GrepSearchMatch {
85 // <file>:<content> when return_line_numbers=false
86 // <file>:<lineno>:<content> when return_line_numbers=true
87 const firstColon = line.indexOf(':');
88 if (firstColon === -1) {
89 return { file: line, content: '' };
91 const file = line.slice(0, firstColon);
92 const tail = line.slice(firstColon + 1);
94 if (!showLineNumbers) {
95 return { file, content: tail };
98 const secondColon = tail.indexOf(':');
99 if (secondColon === -1) {
100 return { file, content: tail };
102 const lineNum = parseInt(tail.slice(0, secondColon), 10);
105 line: Number.isFinite(lineNum) ? lineNum : undefined,
106 content: tail.slice(secondColon + 1)