@adeu/mcp-server 1.14.0 → 1.15.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.js +169 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/formatter.qa.test.ts +71 -0
- package/src/formatter.test.ts +12 -6
- package/src/index.ts +58 -20
- package/src/mcp.bugs.test.ts +22 -1
- package/src/response-builders.ts +151 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatBatchResult } from "./index.js";
|
|
3
|
+
|
|
4
|
+
describe("QA Report V2: Formatter Parity", () => {
|
|
5
|
+
it("F3 & F4: Formats match_mode, occurrences_modified, heading_path, and pages correctly", () => {
|
|
6
|
+
// Mock the stats object that the engine produces when `all` mode is successful
|
|
7
|
+
const stats = {
|
|
8
|
+
actions_applied: 0,
|
|
9
|
+
actions_skipped: 0,
|
|
10
|
+
edits_applied: 1,
|
|
11
|
+
edits_skipped: 0,
|
|
12
|
+
edits: [
|
|
13
|
+
{
|
|
14
|
+
status: "applied",
|
|
15
|
+
target_text: "the Board of Directors",
|
|
16
|
+
new_text: "the Governing Body",
|
|
17
|
+
warning: null,
|
|
18
|
+
error: null,
|
|
19
|
+
critic_markup: "the {--Board of Directors--}{++Governing Body++}",
|
|
20
|
+
clean_text: "the Governing Body",
|
|
21
|
+
pages: [3, 12],
|
|
22
|
+
heading_path: "6. Term > 6.1",
|
|
23
|
+
occurrences_modified: 33,
|
|
24
|
+
match_mode: "all"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
skipped_details: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const res = formatBatchResult(stats, "dummy.docx", false);
|
|
31
|
+
|
|
32
|
+
// Verify the new §5.4.2 visual format
|
|
33
|
+
expect(res).toContain("### Edit 1 ✅ [applied] (p3, p12)");
|
|
34
|
+
expect(res).toContain("**Path:** `6. Term > 6.1`");
|
|
35
|
+
expect(res).toContain("**Mode:** `all` (33 occurrences modified)");
|
|
36
|
+
expect(res).toContain("*Preview (CriticMarkup):*");
|
|
37
|
+
expect(res).toContain("> the {--Board of Directors--}{++Governing Body++}");
|
|
38
|
+
expect(res).toContain("*Preview (Clean):*");
|
|
39
|
+
expect(res).toContain("> the Governing Body");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("R1: Formats dry-run with full enrichment fields identical to real writes", () => {
|
|
43
|
+
const stats = {
|
|
44
|
+
actions_applied: 0,
|
|
45
|
+
actions_skipped: 0,
|
|
46
|
+
edits_applied: 1,
|
|
47
|
+
edits_skipped: 0,
|
|
48
|
+
edits: [
|
|
49
|
+
{
|
|
50
|
+
status: "applied",
|
|
51
|
+
target_text: "Target",
|
|
52
|
+
new_text: "Replaced",
|
|
53
|
+
warning: null,
|
|
54
|
+
error: null,
|
|
55
|
+
critic_markup: "{--Target--}{++Replaced++}",
|
|
56
|
+
clean_text: "Replaced",
|
|
57
|
+
pages: [1],
|
|
58
|
+
heading_path: "1. Intro",
|
|
59
|
+
occurrences_modified: 1,
|
|
60
|
+
match_mode: "all"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
skipped_details: []
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const res = formatBatchResult(stats, "dummy.docx", true);
|
|
67
|
+
expect(res).toContain("**Path:** `1. Intro`");
|
|
68
|
+
expect(res).toContain("**Mode:** `all` (1 occurrence modified)");
|
|
69
|
+
expect(res).toContain("(p1)");
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/formatter.test.ts
CHANGED
|
@@ -16,7 +16,11 @@ describe("MCP Server Tool Output Formatter", () => {
|
|
|
16
16
|
warning: "Warning: target_text contains punctuation.",
|
|
17
17
|
error: null,
|
|
18
18
|
critic_markup: "The {--quick brown fox--}{++fast red fox++} jumps over",
|
|
19
|
-
clean_text: "The fast red fox jumps over"
|
|
19
|
+
clean_text: "The fast red fox jumps over",
|
|
20
|
+
match_mode: "strict",
|
|
21
|
+
occurrences_modified: 1,
|
|
22
|
+
pages: [1],
|
|
23
|
+
heading_path: "1. Intro"
|
|
20
24
|
}
|
|
21
25
|
],
|
|
22
26
|
skipped_details: []
|
|
@@ -29,9 +33,11 @@ describe("MCP Server Tool Output Formatter", () => {
|
|
|
29
33
|
expect(res).toContain("Actions: 0 applied");
|
|
30
34
|
expect(res).toContain("Edits: 1 applied");
|
|
31
35
|
expect(res).toContain("Detailed Edit Reports:");
|
|
32
|
-
expect(res).toContain("✅ [applied]");
|
|
33
|
-
expect(res).toContain("
|
|
34
|
-
expect(res).toContain("
|
|
36
|
+
expect(res).toContain("### Edit 1 ✅ [applied] (p1)");
|
|
37
|
+
expect(res).toContain("**Path:** `1. Intro`");
|
|
38
|
+
expect(res).toContain("**Mode:** `strict` (1 occurrence modified)");
|
|
39
|
+
expect(res).toContain("*Warning:* Warning: target_text contains punctuation.");
|
|
40
|
+
expect(res).toContain("*Preview (CriticMarkup):*\n> The {--quick brown fox--}{++fast red fox++} jumps over");
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
it("formats a failed batch result correctly", () => {
|
|
@@ -57,8 +63,8 @@ describe("MCP Server Tool Output Formatter", () => {
|
|
|
57
63
|
const res = formatBatchResult(stats, "dummy_processed.docx", false);
|
|
58
64
|
|
|
59
65
|
expect(res).toContain("Batch complete. Saved to: dummy_processed.docx");
|
|
60
|
-
expect(res).toContain("❌ [failed]");
|
|
61
|
-
expect(res).toContain("Error
|
|
66
|
+
expect(res).toContain("### Edit 1 ❌ [failed]");
|
|
67
|
+
expect(res).toContain("*Error:* Target text not found in document");
|
|
62
68
|
expect(res).toContain("Skipped Details:\n- Failed to apply edit targeting: 'NON_EXISTENT...'");
|
|
63
69
|
});
|
|
64
70
|
});
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
build_paginated_response,
|
|
25
25
|
build_outline_response,
|
|
26
26
|
build_appendix_response,
|
|
27
|
+
build_search_response,
|
|
27
28
|
} from "./response-builders.js";
|
|
28
29
|
|
|
29
30
|
import { login_to_adeu_cloud, logout_of_adeu_cloud } from "./tools/auth.js";
|
|
@@ -224,17 +225,29 @@ registerAppTool(
|
|
|
224
225
|
"'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.",
|
|
225
226
|
),
|
|
226
227
|
page: z
|
|
227
|
-
.number()
|
|
228
|
+
.union([z.number(), z.string()])
|
|
228
229
|
.default(1)
|
|
229
|
-
.describe("Page number (1-indexed) for mode='full'
|
|
230
|
+
.describe("Page number (1-indexed) for mode='full', or 'all' to return unpaginated."),
|
|
230
231
|
outline_max_level: z
|
|
231
|
-
.number()
|
|
232
|
+
.coerce.number()
|
|
232
233
|
.default(2)
|
|
233
234
|
.describe("For mode='outline' only: cap on heading depth."),
|
|
234
235
|
outline_verbose: z
|
|
235
236
|
.boolean()
|
|
236
237
|
.default(false)
|
|
237
238
|
.describe("For mode='outline' only: includes metadata."),
|
|
239
|
+
search_query: z
|
|
240
|
+
.string()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("The substring or regex pattern to search for. When provided, filters results to matching paragraphs."),
|
|
243
|
+
search_regex: z
|
|
244
|
+
.boolean()
|
|
245
|
+
.default(false)
|
|
246
|
+
.describe("Set to true to interpret search_query as a regular expression."),
|
|
247
|
+
search_case_sensitive: z
|
|
248
|
+
.boolean()
|
|
249
|
+
.default(true)
|
|
250
|
+
.describe("Set to false to perform case-insensitive matching."),
|
|
238
251
|
}),
|
|
239
252
|
_meta: { ui: { resourceUri: MARKDOWN_UI_URI } },
|
|
240
253
|
},
|
|
@@ -245,6 +258,9 @@ registerAppTool(
|
|
|
245
258
|
page,
|
|
246
259
|
outline_max_level,
|
|
247
260
|
outline_verbose,
|
|
261
|
+
search_query,
|
|
262
|
+
search_regex,
|
|
263
|
+
search_case_sensitive,
|
|
248
264
|
}) => {
|
|
249
265
|
try {
|
|
250
266
|
const buf = readFileBytesOrThrow(file_path);
|
|
@@ -267,11 +283,15 @@ registerAppTool(
|
|
|
267
283
|
}
|
|
268
284
|
|
|
269
285
|
const text = await extractTextFromBuffer(buf, clean_view);
|
|
286
|
+
if (search_query !== undefined && search_query !== null) {
|
|
287
|
+
const res = build_search_response(text, search_query, search_regex, search_case_sensitive, page, file_path);
|
|
288
|
+
return res as any;
|
|
289
|
+
}
|
|
270
290
|
if (mode === "appendix") {
|
|
271
|
-
const res = build_appendix_response(text, page, file_path);
|
|
291
|
+
const res = build_appendix_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
|
|
272
292
|
return res as any;
|
|
273
293
|
}
|
|
274
|
-
const res = build_paginated_response(text, page, file_path);
|
|
294
|
+
const res = build_paginated_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
|
|
275
295
|
return res as any;
|
|
276
296
|
} catch (e: any) {
|
|
277
297
|
return {
|
|
@@ -598,10 +618,10 @@ registerAppTool(
|
|
|
598
618
|
has_attachments: z.boolean().optional(),
|
|
599
619
|
attachment_name: z.string().optional(),
|
|
600
620
|
is_unread: z.boolean().optional(),
|
|
601
|
-
days_ago: z.number().optional(),
|
|
621
|
+
days_ago: z.coerce.number().optional(),
|
|
602
622
|
folder: z.enum(["inbox", "sent", "all"]).optional(),
|
|
603
|
-
limit: z.number().default(10),
|
|
604
|
-
offset: z.number().default(0),
|
|
623
|
+
limit: z.coerce.number().default(10),
|
|
624
|
+
offset: z.coerce.number().default(0),
|
|
605
625
|
email_id: z.string().optional(),
|
|
606
626
|
working_directory: z.string().optional(),
|
|
607
627
|
mailbox_address: z
|
|
@@ -613,7 +633,7 @@ registerAppTool(
|
|
|
613
633
|
.optional()
|
|
614
634
|
.describe("If resuming a pending check, provide the task ID here."),
|
|
615
635
|
max_attachment_size_mb: z
|
|
616
|
-
.number()
|
|
636
|
+
.coerce.number()
|
|
617
637
|
.optional()
|
|
618
638
|
.describe(
|
|
619
639
|
"Maximum attachment size in MB to download (default 10). Attachments larger than this are listed in the response but not downloaded. Raise this to fetch large files.",
|
|
@@ -728,8 +748,11 @@ export function formatBatchResult(
|
|
|
728
748
|
} else {
|
|
729
749
|
res = `Batch complete. Saved to: ${outPath}\n`;
|
|
730
750
|
}
|
|
751
|
+
const total_occurrences = stats.edits ? stats.edits.reduce((acc: number, e: any) => acc + (e.status === "applied" ? (e.occurrences_modified || 1) : 0), 0) : 0;
|
|
752
|
+
const occ_text = total_occurrences > stats.edits_applied ? ` (${total_occurrences} occurrences)` : "";
|
|
753
|
+
|
|
731
754
|
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\n`;
|
|
732
|
-
res += `Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.\n`;
|
|
755
|
+
res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\n`;
|
|
733
756
|
|
|
734
757
|
if (stats.edits && stats.edits.length > 0) {
|
|
735
758
|
res += "\nDetailed Edit Reports:\n";
|
|
@@ -737,28 +760,43 @@ export function formatBatchResult(
|
|
|
737
760
|
const report = stats.edits[i];
|
|
738
761
|
const status_indicator =
|
|
739
762
|
report.status === "applied" ? "✅ [applied]" : "❌ [failed]";
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
763
|
+
|
|
764
|
+
const pagesStr = report.pages && report.pages.length > 0
|
|
765
|
+
? ` (p${report.pages.join(", p")})`
|
|
766
|
+
: "";
|
|
767
|
+
|
|
768
|
+
res += `### Edit ${i + 1} ${status_indicator}${pagesStr}\n`;
|
|
769
|
+
|
|
770
|
+
if (report.heading_path) {
|
|
771
|
+
res += `**Path:** \`${report.heading_path}\`\n`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (report.match_mode) {
|
|
775
|
+
const occ = report.occurrences_modified || (report.status === "applied" ? 1 : 0);
|
|
776
|
+
res += `**Mode:** \`${report.match_mode}\` (${occ} occurrence${occ !== 1 ? 's' : ''} modified)\n`;
|
|
745
777
|
}
|
|
778
|
+
|
|
746
779
|
if (report.error) {
|
|
747
|
-
res +=
|
|
780
|
+
res += `*Error:* ${report.error}\n`;
|
|
781
|
+
}
|
|
782
|
+
if (report.warning) {
|
|
783
|
+
res += `*Warning:* ${report.warning}\n`;
|
|
748
784
|
}
|
|
785
|
+
|
|
749
786
|
if (report.critic_markup) {
|
|
750
|
-
res +=
|
|
787
|
+
res += `*Preview (CriticMarkup):*\n> ${report.critic_markup.split('\\n').join('\\n> ')}\n`;
|
|
751
788
|
}
|
|
752
789
|
if (report.clean_text) {
|
|
753
|
-
res +=
|
|
790
|
+
res += `*Preview (Clean):*\n> ${report.clean_text.split('\\n').join('\\n> ')}\n`;
|
|
754
791
|
}
|
|
792
|
+
res += "\n";
|
|
755
793
|
}
|
|
756
794
|
}
|
|
757
795
|
|
|
758
796
|
if (stats.skipped_details && stats.skipped_details.length > 0) {
|
|
759
|
-
res +=
|
|
797
|
+
res += `Skipped Details:\n${stats.skipped_details.join("\n")}`;
|
|
760
798
|
}
|
|
761
|
-
return res;
|
|
799
|
+
return res.trim();
|
|
762
800
|
}
|
|
763
801
|
|
|
764
802
|
// --- Startup ---
|
package/src/mcp.bugs.test.ts
CHANGED
|
@@ -188,5 +188,26 @@ describe("Resolved Bugs MCP Server Verification", () => {
|
|
|
188
188
|
expect(res.result.isError).toBeUndefined();
|
|
189
189
|
expect(res.result.content[0].text).toContain("Dry-run simulation complete.");
|
|
190
190
|
});
|
|
191
|
-
});
|
|
192
191
|
|
|
192
|
+
it("BUG-12: Accepts stringified numbers for numeric arguments without Zod validation errors", async () => {
|
|
193
|
+
const res = await sendRpc(
|
|
194
|
+
"tools/call",
|
|
195
|
+
{
|
|
196
|
+
name: "read_docx",
|
|
197
|
+
arguments: {
|
|
198
|
+
file_path: cleanDocPath,
|
|
199
|
+
page: "1",
|
|
200
|
+
outline_max_level: "3",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
106,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// If Zod validation failed, we would get an error payload back or `res.error`
|
|
207
|
+
// from the MCP protocol (error code -32602).
|
|
208
|
+
expect(res.error).toBeUndefined();
|
|
209
|
+
expect(res.result).toBeDefined();
|
|
210
|
+
expect(res.result.isError).toBeUndefined();
|
|
211
|
+
expect(res.result.content[0].text).toContain("golden");
|
|
212
|
+
});
|
|
213
|
+
});
|
package/src/response-builders.ts
CHANGED
|
@@ -208,3 +208,154 @@ export function build_appendix_response(
|
|
|
208
208
|
},
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
export function build_search_response(
|
|
213
|
+
text: string,
|
|
214
|
+
search_query: string,
|
|
215
|
+
search_regex: boolean,
|
|
216
|
+
search_case_sensitive: boolean,
|
|
217
|
+
page: number | string,
|
|
218
|
+
file_path: string,
|
|
219
|
+
): ToolResult {
|
|
220
|
+
const [body] = split_structural_appendix(text);
|
|
221
|
+
const escapeRegExp = (s: string) =>
|
|
222
|
+
s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
223
|
+
const flags = search_case_sensitive ? "g" : "gi";
|
|
224
|
+
const patternStr = search_regex ? search_query : escapeRegExp(search_query);
|
|
225
|
+
|
|
226
|
+
let regex: RegExp;
|
|
227
|
+
try {
|
|
228
|
+
regex = new RegExp(patternStr, flags);
|
|
229
|
+
} catch (e: any) {
|
|
230
|
+
throw new Error(`Invalid regex pattern: ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const matches = Array.from(body.matchAll(regex));
|
|
234
|
+
|
|
235
|
+
if (matches.length === 0) {
|
|
236
|
+
const ui_markdown = `> **Search Results** — No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.\n\nVerify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
|
|
237
|
+
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: llm_content }],
|
|
240
|
+
structuredContent: {
|
|
241
|
+
markdown: ui_markdown,
|
|
242
|
+
title: `Search: ${basename(file_path)}`,
|
|
243
|
+
file_path: resolve(file_path),
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const pag_res = paginate(body, "");
|
|
249
|
+
const page_offsets = pag_res.body_page_offsets;
|
|
250
|
+
const total_matches = matches.length;
|
|
251
|
+
const total_pages = Math.ceil(total_matches / 10);
|
|
252
|
+
|
|
253
|
+
let start_idx = 0;
|
|
254
|
+
let end_idx = total_matches;
|
|
255
|
+
let page_text = "all";
|
|
256
|
+
|
|
257
|
+
const pageStr = String(page).toLowerCase();
|
|
258
|
+
if (pageStr !== "all") {
|
|
259
|
+
const page_num = parseInt(pageStr, 10);
|
|
260
|
+
if (isNaN(page_num) || page_num < 1 || page_num > total_pages) {
|
|
261
|
+
throw new Error(`Page ${page} out of range (search has ${total_pages} pages).`);
|
|
262
|
+
}
|
|
263
|
+
start_idx = (page_num - 1) * 10;
|
|
264
|
+
end_idx = Math.min(start_idx + 10, total_matches);
|
|
265
|
+
page_text = `${page_num} of ${total_pages}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const page_matches = matches.slice(start_idx, end_idx);
|
|
269
|
+
|
|
270
|
+
const ui_parts: string[] = [
|
|
271
|
+
`> **Search Results** — Found ${total_matches} matches for query \`${search_query}\` in \`${basename(file_path)}\`.`,
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
if (total_pages > 1 && pageStr !== "all") {
|
|
275
|
+
const nextPage = parseInt(pageStr, 10) + 1;
|
|
276
|
+
ui_parts.push(`> Showing page ${page_text} (matches ${start_idx + 1}-${end_idx}). To see more matches, call \`read_docx\` with \`search_query='${search_query}'\`, \`search_regex=${search_regex ? "true" : "false"}\`, and \`page=${nextPage}\`.`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const occurrences_map: Record<string, number> = {};
|
|
280
|
+
for (const m of matches) {
|
|
281
|
+
const matched_str = m[0];
|
|
282
|
+
occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function get_heading(idx: number, txt: string): string {
|
|
286
|
+
const txtBefore = txt.substring(0, idx);
|
|
287
|
+
const lines = txtBefore.split("\n");
|
|
288
|
+
const path: string[] = [];
|
|
289
|
+
let current_level = 999;
|
|
290
|
+
|
|
291
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
292
|
+
const line = lines[i];
|
|
293
|
+
const m = line.match(/^(#{1,6})\s+(.*)/);
|
|
294
|
+
if (m) {
|
|
295
|
+
const level = m[1].length;
|
|
296
|
+
if (level < current_level) {
|
|
297
|
+
let cleanHeading = m[2].replace(/\*\*|__|[*_]/g, "").replace(/\{#[^}]+\}/g, "").trim();
|
|
298
|
+
if (cleanHeading.length > 80) {
|
|
299
|
+
cleanHeading = cleanHeading.substring(0, 80) + "...";
|
|
300
|
+
}
|
|
301
|
+
path.unshift(cleanHeading);
|
|
302
|
+
current_level = level;
|
|
303
|
+
if (level === 1) break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return path.join(" > ");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let i = start_idx + 1;
|
|
311
|
+
for (const m of page_matches) {
|
|
312
|
+
const matched_str = m[0];
|
|
313
|
+
const m_start = m.index!;
|
|
314
|
+
const m_end = m_start + matched_str.length;
|
|
315
|
+
|
|
316
|
+
let p_num = 1;
|
|
317
|
+
for (let j = 0; j < page_offsets.length; j++) {
|
|
318
|
+
if (m_start >= page_offsets[j]) {
|
|
319
|
+
p_num = j + 1;
|
|
320
|
+
} else {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const snippet_start = Math.max(0, m_start - 100);
|
|
326
|
+
const snippet_end = Math.min(body.length, m_end + 100);
|
|
327
|
+
const snippet = body.substring(snippet_start, m_start) + `**${matched_str}**` + body.substring(m_end, snippet_end);
|
|
328
|
+
|
|
329
|
+
const snippet_lines = snippet
|
|
330
|
+
.split("\n")
|
|
331
|
+
.filter((line) => line.trim().length > 0)
|
|
332
|
+
.map((line) => `> ${line}`)
|
|
333
|
+
.join("\n");
|
|
334
|
+
|
|
335
|
+
ui_parts.push("---");
|
|
336
|
+
ui_parts.push(`### Match ${i} (p${p_num})`);
|
|
337
|
+
|
|
338
|
+
const h_path = get_heading(m_start, body);
|
|
339
|
+
if (h_path) {
|
|
340
|
+
ui_parts.push(`**Path:** \`${h_path}\``);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const count = occurrences_map[matched_str];
|
|
344
|
+
ui_parts.push(snippet_lines);
|
|
345
|
+
ui_parts.push(`*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? "s" : ""} in the document.`);
|
|
346
|
+
|
|
347
|
+
i++;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const ui_markdown = ui_parts.join("\n\n");
|
|
351
|
+
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: "text", text: llm_content }],
|
|
355
|
+
structuredContent: {
|
|
356
|
+
markdown: ui_markdown,
|
|
357
|
+
title: `Search: ${basename(file_path)}`,
|
|
358
|
+
file_path: resolve(file_path),
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|