@adeu/mcp-server 1.15.1 → 1.15.2
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 +118 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +217 -173
- package/src/response-builders.ts +118 -50
- package/src/response_builders.test.ts +266 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeu/mcp-server",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"mcpName": "ai.adeu/adeu",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"type": "module",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@adeu/core": "^1.15.
|
|
34
|
+
"@adeu/core": "^1.15.2",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"@modelcontextprotocol/ext-apps": "^1.0.0",
|
|
37
37
|
"zod": "^3.23.8"
|
package/src/index.ts
CHANGED
|
@@ -42,17 +42,17 @@ function readFileBytesOrThrow(filePath: string): Buffer {
|
|
|
42
42
|
if (err.code === "ENOENT") {
|
|
43
43
|
throw new Error(
|
|
44
44
|
`File not found: ${filePath}.\n` +
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
`If you are running in a sandboxed/containerized environment (such as Claude Desktop or another containerized client), ` +
|
|
46
|
+
`the host application or MCP server may not have direct access to your local workspace files.\n` +
|
|
47
|
+
`You can resolve this by installing and running the local 'adeu' CLI tool directly within your environment.\n` +
|
|
48
|
+
`Here is how the MCP tools map to their CLI equivalents:\n` +
|
|
49
|
+
`- read_docx -> adeu extract ${filePath}\n` +
|
|
50
|
+
`- process_document_batch -> adeu apply ${filePath}\n` +
|
|
51
|
+
`- diff_docx_files -> adeu diff ${filePath} <modified_path>\n` +
|
|
52
|
+
`- accept_all_changes -> adeu accept-all ${filePath}\n\n` +
|
|
53
|
+
`To run the local tool, install it via:\n` +
|
|
54
|
+
` uv tool install adeu\n` +
|
|
55
|
+
`and run the mapped CLI command directly in your terminal.`,
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
throw err;
|
|
@@ -95,7 +95,9 @@ const buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
|
|
|
95
95
|
// --- Scope Configuration ---
|
|
96
96
|
const args = process.argv.slice(2);
|
|
97
97
|
const scopeIdx = args.indexOf("--scope");
|
|
98
|
-
const requestedScope = (
|
|
98
|
+
const requestedScope = (
|
|
99
|
+
scopeIdx !== -1 ? args[scopeIdx + 1] : "all"
|
|
100
|
+
).toLowerCase();
|
|
99
101
|
const isDocxOnly = requestedScope === "docx";
|
|
100
102
|
|
|
101
103
|
// --- Server Setup ---
|
|
@@ -116,7 +118,12 @@ server.registerTool = (name: string, schema: any, handler?: any) => {
|
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
// Wrap registerAppTool to inject buildTag into descriptions
|
|
119
|
-
const registerAppTool: typeof origRegisterAppTool = (
|
|
121
|
+
const registerAppTool: typeof origRegisterAppTool = (
|
|
122
|
+
mcpServer,
|
|
123
|
+
name,
|
|
124
|
+
schema,
|
|
125
|
+
handler,
|
|
126
|
+
) => {
|
|
120
127
|
if (schema && typeof schema === "object") {
|
|
121
128
|
if (schema.description) {
|
|
122
129
|
schema.description = schema.description.trim() + buildTag;
|
|
@@ -226,10 +233,12 @@ registerAppTool(
|
|
|
226
233
|
),
|
|
227
234
|
page: z
|
|
228
235
|
.union([z.number(), z.string()])
|
|
229
|
-
.
|
|
230
|
-
.describe(
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
.optional()
|
|
237
|
+
.describe(
|
|
238
|
+
"Without `search_query`: 1-indexed document page to display (defaults to 1). With `search_query`: restricts matches to that document page (defaults to searching all pages; pass `page='all'` to be explicit).",
|
|
239
|
+
),
|
|
240
|
+
outline_max_level: z.coerce
|
|
241
|
+
.number()
|
|
233
242
|
.default(2)
|
|
234
243
|
.describe("For mode='outline' only: cap on heading depth."),
|
|
235
244
|
outline_verbose: z
|
|
@@ -239,11 +248,15 @@ registerAppTool(
|
|
|
239
248
|
search_query: z
|
|
240
249
|
.string()
|
|
241
250
|
.optional()
|
|
242
|
-
.describe(
|
|
251
|
+
.describe(
|
|
252
|
+
"The substring or regex pattern to search for. When provided, filters results to matching paragraphs.",
|
|
253
|
+
),
|
|
243
254
|
search_regex: z
|
|
244
255
|
.boolean()
|
|
245
256
|
.default(false)
|
|
246
|
-
.describe(
|
|
257
|
+
.describe(
|
|
258
|
+
"Set to true to interpret search_query as a regular expression.",
|
|
259
|
+
),
|
|
247
260
|
search_case_sensitive: z
|
|
248
261
|
.boolean()
|
|
249
262
|
.default(true)
|
|
@@ -267,7 +280,12 @@ registerAppTool(
|
|
|
267
280
|
|
|
268
281
|
if (mode === "outline") {
|
|
269
282
|
const doc = await DocumentObject.load(buf);
|
|
270
|
-
const extract_res = _extractTextFromDoc(
|
|
283
|
+
const extract_res = _extractTextFromDoc(
|
|
284
|
+
doc,
|
|
285
|
+
clean_view,
|
|
286
|
+
true,
|
|
287
|
+
true,
|
|
288
|
+
) as {
|
|
271
289
|
text: string;
|
|
272
290
|
paragraph_offsets: Map<any, [number, number]>;
|
|
273
291
|
};
|
|
@@ -284,14 +302,29 @@ registerAppTool(
|
|
|
284
302
|
|
|
285
303
|
const text = await extractTextFromBuffer(buf, clean_view);
|
|
286
304
|
if (search_query !== undefined && search_query !== null) {
|
|
287
|
-
|
|
305
|
+
// In search mode, undefined `page` means "search all document pages".
|
|
306
|
+
const res = build_search_response(
|
|
307
|
+
text,
|
|
308
|
+
search_query,
|
|
309
|
+
search_regex,
|
|
310
|
+
search_case_sensitive,
|
|
311
|
+
page,
|
|
312
|
+
file_path,
|
|
313
|
+
);
|
|
288
314
|
return res as any;
|
|
289
315
|
}
|
|
316
|
+
// In non-search mode, `page` defaults to 1 (show document page 1).
|
|
317
|
+
const resolvedPage =
|
|
318
|
+
page === undefined || page === null
|
|
319
|
+
? 1
|
|
320
|
+
: typeof page === "number"
|
|
321
|
+
? page
|
|
322
|
+
: parseInt(String(page), 10) || 1;
|
|
290
323
|
if (mode === "appendix") {
|
|
291
|
-
const res = build_appendix_response(text,
|
|
324
|
+
const res = build_appendix_response(text, resolvedPage, file_path);
|
|
292
325
|
return res as any;
|
|
293
326
|
}
|
|
294
|
-
const res = build_paginated_response(text,
|
|
327
|
+
const res = build_paginated_response(text, resolvedPage, file_path);
|
|
295
328
|
return res as any;
|
|
296
329
|
} catch (e: any) {
|
|
297
330
|
return {
|
|
@@ -595,145 +628,145 @@ server.registerTool(
|
|
|
595
628
|
);
|
|
596
629
|
|
|
597
630
|
if (!isDocxOnly) {
|
|
598
|
-
registerAppTool(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
);
|
|
631
|
+
registerAppTool(
|
|
632
|
+
server,
|
|
633
|
+
"search_and_fetch_emails",
|
|
634
|
+
{
|
|
635
|
+
title: "Search & Fetch Emails",
|
|
636
|
+
description:
|
|
637
|
+
"Searches the user's live email inbox via the Adeu cloud backend.\n\n" +
|
|
638
|
+
"TWO MODES:\n" +
|
|
639
|
+
"1. Search mode (no `email_id`): returns up to `limit` lightweight previews. Use filters (`sender`, `subject`, `is_unread`, `days_ago`, `folder`, `has_attachments`, `attachment_name`) to narrow down.\n" +
|
|
640
|
+
"2. Fetch mode (with `email_id`): returns the full email body, thread history, and downloads attachments under `max_attachment_size_mb` to the local disk.\n\n" +
|
|
641
|
+
"AUTO-ESCALATION: If a search returns exactly one preview, the backend automatically fetches the full email in the same call. Plan around the response shape — check the `type` field (`previews` vs `full_email`) before assuming.\n\n" +
|
|
642
|
+
"EMAIL ID FORMATS (`email_id` parameter accepts any of):\n" +
|
|
643
|
+
"- `msg_<6 chars>` — short ID returned by previews on THIS machine. NOT portable across machines or sessions; the local cache holds the most recent 1000. If you reference one that's been evicted, the tool returns a StaleShortIdError telling you to re-search.\n" +
|
|
644
|
+
"- `adeu_<numeric>` — server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n" +
|
|
645
|
+
"- Raw provider ID (Gmail/Outlook native ID) — works if you have it, but you usually won't.\n\n" +
|
|
646
|
+
"FOLDER DEFAULT: omitting `folder` searches the Inbox only (matching what the user sees in their mail client). Use `folder='sent'` for sent items, `folder='all'` to include Deleted Items, Drafts, and other folders.\n\n" +
|
|
647
|
+
"ATTACHMENTS: attachments larger than `max_attachment_size_mb` (default 10) are listed in the response but NOT downloaded — raise the cap if you need them. Always set `working_directory` when calling from a project so attachments land alongside the user's other files.",
|
|
648
|
+
inputSchema: z.object({
|
|
649
|
+
sender: z.string().optional(),
|
|
650
|
+
subject: z.string().optional(),
|
|
651
|
+
has_attachments: z.boolean().optional(),
|
|
652
|
+
attachment_name: z.string().optional(),
|
|
653
|
+
is_unread: z.boolean().optional(),
|
|
654
|
+
days_ago: z.coerce.number().optional(),
|
|
655
|
+
folder: z.enum(["inbox", "sent", "all"]).optional(),
|
|
656
|
+
limit: z.coerce.number().default(10),
|
|
657
|
+
offset: z.coerce.number().default(0),
|
|
658
|
+
email_id: z.string().optional(),
|
|
659
|
+
working_directory: z.string().optional(),
|
|
660
|
+
mailbox_address: z
|
|
661
|
+
.string()
|
|
662
|
+
.optional()
|
|
663
|
+
.describe("Optional target mailbox email address to search within."),
|
|
664
|
+
task_id: z
|
|
665
|
+
.string()
|
|
666
|
+
.optional()
|
|
667
|
+
.describe("If resuming a pending check, provide the task ID here."),
|
|
668
|
+
max_attachment_size_mb: z.coerce
|
|
669
|
+
.number()
|
|
670
|
+
.optional()
|
|
671
|
+
.describe(
|
|
672
|
+
"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.",
|
|
673
|
+
),
|
|
674
|
+
}),
|
|
675
|
+
_meta: { ui: { resourceUri: EMAIL_UI_URI } },
|
|
676
|
+
},
|
|
677
|
+
async (args) => {
|
|
678
|
+
try {
|
|
679
|
+
return (await search_and_fetch_emails(args)) as any;
|
|
680
|
+
} catch (e: any) {
|
|
681
|
+
return {
|
|
682
|
+
isError: true,
|
|
683
|
+
content: [{ type: "text", text: e.message }],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
);
|
|
655
688
|
|
|
656
|
-
server.registerTool(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
689
|
+
server.registerTool(
|
|
690
|
+
"login_to_adeu_cloud",
|
|
691
|
+
{
|
|
692
|
+
description:
|
|
693
|
+
"Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\n" +
|
|
694
|
+
"IMPORTANT — login is user-level, not account-level:\n" +
|
|
695
|
+
"- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n" +
|
|
696
|
+
"- Signing in through ANY of the user's linked accounts authenticates the same Adeu user. Once logged in, the session can read from and draft in ALL of that user's linked accounts and ALL of their mailboxes — not just the one used to sign in.\n" +
|
|
697
|
+
"- The choice of which provider account to sign in through is purely an SSO mechanism; it does not select a 'current account' for the session.\n\n" +
|
|
698
|
+
"When the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response.",
|
|
699
|
+
},
|
|
700
|
+
async () => {
|
|
701
|
+
try {
|
|
702
|
+
return (await login_to_adeu_cloud()) as any;
|
|
703
|
+
} catch (e: any) {
|
|
704
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
);
|
|
675
708
|
|
|
676
|
-
server.registerTool(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
},
|
|
686
|
-
);
|
|
687
|
-
server.registerTool(
|
|
688
|
-
"create_email_draft",
|
|
689
|
-
{
|
|
690
|
-
description:
|
|
691
|
-
"Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\n" +
|
|
692
|
-
"TWO MODES:\n" +
|
|
693
|
-
"1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original — do NOT pass `subject` or `to_recipients`.\n" +
|
|
694
|
-
"2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n" +
|
|
695
|
-
"`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n" +
|
|
696
|
-
"`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown — do not pre-render HTML.\n\n" +
|
|
697
|
-
"`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments — those local paths can be passed directly here.",
|
|
698
|
-
inputSchema: {
|
|
699
|
-
body_markdown: z.string(),
|
|
700
|
-
reply_to_email_id: z.string().optional(),
|
|
701
|
-
subject: z.string().optional(),
|
|
702
|
-
to_recipients: z.array(z.string()).optional(),
|
|
703
|
-
attachment_paths: z.array(z.string()).optional(),
|
|
704
|
-
mailbox_address: z
|
|
705
|
-
.string()
|
|
706
|
-
.optional()
|
|
707
|
-
.describe(
|
|
708
|
-
"Optional target mailbox email address to create the draft in.",
|
|
709
|
-
),
|
|
709
|
+
server.registerTool(
|
|
710
|
+
"logout_of_adeu_cloud",
|
|
711
|
+
{ description: "Logs out of the Adeu Cloud backend." },
|
|
712
|
+
async () => {
|
|
713
|
+
try {
|
|
714
|
+
return (await logout_of_adeu_cloud()) as any;
|
|
715
|
+
} catch (e: any) {
|
|
716
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
717
|
+
}
|
|
710
718
|
},
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
719
|
+
);
|
|
720
|
+
server.registerTool(
|
|
721
|
+
"create_email_draft",
|
|
722
|
+
{
|
|
723
|
+
description:
|
|
724
|
+
"Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\n" +
|
|
725
|
+
"TWO MODES:\n" +
|
|
726
|
+
"1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original — do NOT pass `subject` or `to_recipients`.\n" +
|
|
727
|
+
"2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n" +
|
|
728
|
+
"`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n" +
|
|
729
|
+
"`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown — do not pre-render HTML.\n\n" +
|
|
730
|
+
"`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments — those local paths can be passed directly here.",
|
|
731
|
+
inputSchema: {
|
|
732
|
+
body_markdown: z.string(),
|
|
733
|
+
reply_to_email_id: z.string().optional(),
|
|
734
|
+
subject: z.string().optional(),
|
|
735
|
+
to_recipients: z.array(z.string()).optional(),
|
|
736
|
+
attachment_paths: z.array(z.string()).optional(),
|
|
737
|
+
mailbox_address: z
|
|
738
|
+
.string()
|
|
739
|
+
.optional()
|
|
740
|
+
.describe(
|
|
741
|
+
"Optional target mailbox email address to create the draft in.",
|
|
742
|
+
),
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
async (args) => {
|
|
746
|
+
try {
|
|
747
|
+
return (await create_email_draft(args)) as any;
|
|
748
|
+
} catch (e: any) {
|
|
749
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
server.registerTool(
|
|
754
|
+
"list_available_mailboxes",
|
|
755
|
+
{
|
|
756
|
+
description:
|
|
757
|
+
"Lists all personal and shared/delegated mailboxes the authenticated Adeu user has access to, across ALL of their linked provider accounts. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\n" +
|
|
758
|
+
"This is the right tool to answer 'which accounts/mailboxes am I logged into?' — Adeu login is user-level, so a single MCP session can see every mailbox listed here regardless of which provider account was used for SSO.\n\n" +
|
|
759
|
+
"Call this FIRST when the user names a specific mailbox or shared inbox, to resolve the canonical `email_address`. Then pass that address as `mailbox_address` to `search_and_fetch_emails` or `create_email_draft` to scope the operation. Omitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
|
|
760
|
+
inputSchema: {},
|
|
761
|
+
},
|
|
762
|
+
async () => {
|
|
763
|
+
try {
|
|
764
|
+
return (await list_available_mailboxes()) as any;
|
|
765
|
+
} catch (e: any) {
|
|
766
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
);
|
|
737
770
|
}
|
|
738
771
|
|
|
739
772
|
// --- Formatter for process_document_batch ---
|
|
@@ -748,8 +781,17 @@ export function formatBatchResult(
|
|
|
748
781
|
} else {
|
|
749
782
|
res = `Batch complete. Saved to: ${outPath}\n`;
|
|
750
783
|
}
|
|
751
|
-
const total_occurrences = stats.edits
|
|
752
|
-
|
|
784
|
+
const total_occurrences = stats.edits
|
|
785
|
+
? stats.edits.reduce(
|
|
786
|
+
(acc: number, e: any) =>
|
|
787
|
+
acc + (e.status === "applied" ? e.occurrences_modified || 1 : 0),
|
|
788
|
+
0,
|
|
789
|
+
)
|
|
790
|
+
: 0;
|
|
791
|
+
const occ_text =
|
|
792
|
+
total_occurrences > stats.edits_applied
|
|
793
|
+
? ` (${total_occurrences} occurrences)`
|
|
794
|
+
: "";
|
|
753
795
|
|
|
754
796
|
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\n`;
|
|
755
797
|
res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\n`;
|
|
@@ -760,20 +802,22 @@ export function formatBatchResult(
|
|
|
760
802
|
const report = stats.edits[i];
|
|
761
803
|
const status_indicator =
|
|
762
804
|
report.status === "applied" ? "✅ [applied]" : "❌ [failed]";
|
|
763
|
-
|
|
764
|
-
const pagesStr =
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
805
|
+
|
|
806
|
+
const pagesStr =
|
|
807
|
+
report.pages && report.pages.length > 0
|
|
808
|
+
? ` (p${report.pages.join(", p")})`
|
|
809
|
+
: "";
|
|
810
|
+
|
|
768
811
|
res += `### Edit ${i + 1} ${status_indicator}${pagesStr}\n`;
|
|
769
|
-
|
|
812
|
+
|
|
770
813
|
if (report.heading_path) {
|
|
771
814
|
res += `**Path:** \`${report.heading_path}\`\n`;
|
|
772
815
|
}
|
|
773
|
-
|
|
816
|
+
|
|
774
817
|
if (report.match_mode) {
|
|
775
|
-
|
|
776
|
-
|
|
818
|
+
const occ =
|
|
819
|
+
report.occurrences_modified || (report.status === "applied" ? 1 : 0);
|
|
820
|
+
res += `**Mode:** \`${report.match_mode}\` (${occ} occurrence${occ !== 1 ? "s" : ""} modified)\n`;
|
|
777
821
|
}
|
|
778
822
|
|
|
779
823
|
if (report.error) {
|
|
@@ -782,12 +826,12 @@ export function formatBatchResult(
|
|
|
782
826
|
if (report.warning) {
|
|
783
827
|
res += `*Warning:* ${report.warning}\n`;
|
|
784
828
|
}
|
|
785
|
-
|
|
829
|
+
|
|
786
830
|
if (report.critic_markup) {
|
|
787
|
-
res += `*Preview (CriticMarkup):*\n> ${report.critic_markup.split(
|
|
831
|
+
res += `*Preview (CriticMarkup):*\n> ${report.critic_markup.split("\\n").join("\\n> ")}\n`;
|
|
788
832
|
}
|
|
789
833
|
if (report.clean_text) {
|
|
790
|
-
res += `*Preview (Clean):*\n> ${report.clean_text.split(
|
|
834
|
+
res += `*Preview (Clean):*\n> ${report.clean_text.split("\\n").join("\\n> ")}\n`;
|
|
791
835
|
}
|
|
792
836
|
res += "\n";
|
|
793
837
|
}
|