@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeu/mcp-server",
3
- "version": "1.15.1",
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.1",
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
- `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.`
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 = (scopeIdx !== -1 ? args[scopeIdx + 1] : "all").toLowerCase();
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 = (mcpServer, name, schema, handler) => {
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
- .default(1)
230
- .describe("Page number (1-indexed) for mode='full', or 'all' to return unpaginated."),
231
- outline_max_level: z
232
- .coerce.number()
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("The substring or regex pattern to search for. When provided, filters results to matching paragraphs."),
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("Set to true to interpret search_query as a regular expression."),
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(doc, clean_view, true, true) as {
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
- const res = build_search_response(text, search_query, search_regex, search_case_sensitive, page, file_path);
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, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
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, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
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
- server,
600
- "search_and_fetch_emails",
601
- {
602
- title: "Search & Fetch Emails",
603
- description:
604
- "Searches the user's live email inbox via the Adeu cloud backend.\n\n" +
605
- "TWO MODES:\n" +
606
- "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" +
607
- "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" +
608
- "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" +
609
- "EMAIL ID FORMATS (`email_id` parameter accepts any of):\n" +
610
- "- `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" +
611
- "- `adeu_<numeric>` — server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n" +
612
- "- Raw provider ID (Gmail/Outlook native ID) — works if you have it, but you usually won't.\n\n" +
613
- "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" +
614
- "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.",
615
- inputSchema: z.object({
616
- sender: z.string().optional(),
617
- subject: z.string().optional(),
618
- has_attachments: z.boolean().optional(),
619
- attachment_name: z.string().optional(),
620
- is_unread: z.boolean().optional(),
621
- days_ago: z.coerce.number().optional(),
622
- folder: z.enum(["inbox", "sent", "all"]).optional(),
623
- limit: z.coerce.number().default(10),
624
- offset: z.coerce.number().default(0),
625
- email_id: z.string().optional(),
626
- working_directory: z.string().optional(),
627
- mailbox_address: z
628
- .string()
629
- .optional()
630
- .describe("Optional target mailbox email address to search within."),
631
- task_id: z
632
- .string()
633
- .optional()
634
- .describe("If resuming a pending check, provide the task ID here."),
635
- max_attachment_size_mb: z
636
- .coerce.number()
637
- .optional()
638
- .describe(
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.",
640
- ),
641
- }),
642
- _meta: { ui: { resourceUri: EMAIL_UI_URI } },
643
- },
644
- async (args) => {
645
- try {
646
- return (await search_and_fetch_emails(args)) as any;
647
- } catch (e: any) {
648
- return {
649
- isError: true,
650
- content: [{ type: "text", text: e.message }],
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
- "login_to_adeu_cloud",
658
- {
659
- description:
660
- "Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\n" +
661
- "IMPORTANT — login is user-level, not account-level:\n" +
662
- "- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n" +
663
- "- 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" +
664
- "- 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" +
665
- "When the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response.",
666
- },
667
- async () => {
668
- try {
669
- return (await login_to_adeu_cloud()) as any;
670
- } catch (e: any) {
671
- return { isError: true, content: [{ type: "text", text: e.message }] };
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
- "logout_of_adeu_cloud",
678
- { description: "Logs out of the Adeu Cloud backend." },
679
- async () => {
680
- try {
681
- return (await logout_of_adeu_cloud()) as any;
682
- } catch (e: any) {
683
- return { isError: true, content: [{ type: "text", text: e.message }] };
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
- async (args) => {
713
- try {
714
- return (await create_email_draft(args)) as any;
715
- } catch (e: any) {
716
- return { isError: true, content: [{ type: "text", text: e.message }] };
717
- }
718
- },
719
- );
720
- server.registerTool(
721
- "list_available_mailboxes",
722
- {
723
- description:
724
- "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" +
725
- "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" +
726
- "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.",
727
- inputSchema: {},
728
- },
729
- async () => {
730
- try {
731
- return (await list_available_mailboxes()) as any;
732
- } catch (e: any) {
733
- return { isError: true, content: [{ type: "text", text: e.message }] };
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 ? 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)` : "";
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 = report.pages && report.pages.length > 0
765
- ? ` (p${report.pages.join(", p")})`
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
- const occ = report.occurrences_modified || (report.status === "applied" ? 1 : 0);
776
- res += `**Mode:** \`${report.match_mode}\` (${occ} occurrence${occ !== 1 ? 's' : ''} modified)\n`;
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('\\n').join('\\n> ')}\n`;
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('\\n').join('\\n> ')}\n`;
834
+ res += `*Preview (Clean):*\n> ${report.clean_text.split("\\n").join("\\n> ")}\n`;
791
835
  }
792
836
  res += "\n";
793
837
  }