@documonster/mcp 0.8.0-canary.sha.09791dab

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.
Files changed (127) hide show
  1. package/LICENSE +205 -0
  2. package/README.md +253 -0
  3. package/dist/capabilities.d.ts +29 -0
  4. package/dist/capabilities.d.ts.map +1 -0
  5. package/dist/capabilities.js +188 -0
  6. package/dist/capabilities.js.map +1 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +64 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +65 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +189 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/errors.d.ts +66 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +96 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/index.d.ts +25 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +20 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/sandbox.d.ts +86 -0
  24. package/dist/sandbox.d.ts.map +1 -0
  25. package/dist/sandbox.js +224 -0
  26. package/dist/sandbox.js.map +1 -0
  27. package/dist/server.d.ts +22 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +66 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/tools/archive-read.d.ts +31 -0
  32. package/dist/tools/archive-read.d.ts.map +1 -0
  33. package/dist/tools/archive-read.js +315 -0
  34. package/dist/tools/archive-read.js.map +1 -0
  35. package/dist/tools/archive-write.d.ts +9 -0
  36. package/dist/tools/archive-write.d.ts.map +1 -0
  37. package/dist/tools/archive-write.js +219 -0
  38. package/dist/tools/archive-write.js.map +1 -0
  39. package/dist/tools/chart.d.ts +93 -0
  40. package/dist/tools/chart.d.ts.map +1 -0
  41. package/dist/tools/chart.js +263 -0
  42. package/dist/tools/chart.js.map +1 -0
  43. package/dist/tools/doc-convert.d.ts +13 -0
  44. package/dist/tools/doc-convert.d.ts.map +1 -0
  45. package/dist/tools/doc-convert.js +209 -0
  46. package/dist/tools/doc-convert.js.map +1 -0
  47. package/dist/tools/doc-paginate.d.ts +14 -0
  48. package/dist/tools/doc-paginate.d.ts.map +1 -0
  49. package/dist/tools/doc-paginate.js +153 -0
  50. package/dist/tools/doc-paginate.js.map +1 -0
  51. package/dist/tools/doc-read.d.ts +14 -0
  52. package/dist/tools/doc-read.d.ts.map +1 -0
  53. package/dist/tools/doc-read.js +175 -0
  54. package/dist/tools/doc-read.js.map +1 -0
  55. package/dist/tools/doc-review.d.ts +17 -0
  56. package/dist/tools/doc-review.d.ts.map +1 -0
  57. package/dist/tools/doc-review.js +249 -0
  58. package/dist/tools/doc-review.js.map +1 -0
  59. package/dist/tools/doc-search.d.ts +20 -0
  60. package/dist/tools/doc-search.d.ts.map +1 -0
  61. package/dist/tools/doc-search.js +268 -0
  62. package/dist/tools/doc-search.js.map +1 -0
  63. package/dist/tools/doc-write.d.ts +11 -0
  64. package/dist/tools/doc-write.d.ts.map +1 -0
  65. package/dist/tools/doc-write.js +81 -0
  66. package/dist/tools/doc-write.js.map +1 -0
  67. package/dist/tools/document.d.ts +74 -0
  68. package/dist/tools/document.d.ts.map +1 -0
  69. package/dist/tools/document.js +157 -0
  70. package/dist/tools/document.js.map +1 -0
  71. package/dist/tools/form-fill.d.ts +20 -0
  72. package/dist/tools/form-fill.d.ts.map +1 -0
  73. package/dist/tools/form-fill.js +270 -0
  74. package/dist/tools/form-fill.js.map +1 -0
  75. package/dist/tools/formula-evaluate.d.ts +14 -0
  76. package/dist/tools/formula-evaluate.d.ts.map +1 -0
  77. package/dist/tools/formula-evaluate.js +192 -0
  78. package/dist/tools/formula-evaluate.js.map +1 -0
  79. package/dist/tools/fs-helpers.d.ts +106 -0
  80. package/dist/tools/fs-helpers.d.ts.map +1 -0
  81. package/dist/tools/fs-helpers.js +257 -0
  82. package/dist/tools/fs-helpers.js.map +1 -0
  83. package/dist/tools/help.d.ts +227 -0
  84. package/dist/tools/help.d.ts.map +1 -0
  85. package/dist/tools/help.js +262 -0
  86. package/dist/tools/help.js.map +1 -0
  87. package/dist/tools/index.d.ts +33 -0
  88. package/dist/tools/index.d.ts.map +1 -0
  89. package/dist/tools/index.js +78 -0
  90. package/dist/tools/index.js.map +1 -0
  91. package/dist/tools/inspect.d.ts +14 -0
  92. package/dist/tools/inspect.d.ts.map +1 -0
  93. package/dist/tools/inspect.js +463 -0
  94. package/dist/tools/inspect.js.map +1 -0
  95. package/dist/tools/pdf-edit.d.ts +26 -0
  96. package/dist/tools/pdf-edit.d.ts.map +1 -0
  97. package/dist/tools/pdf-edit.js +390 -0
  98. package/dist/tools/pdf-edit.js.map +1 -0
  99. package/dist/tools/result.d.ts +25 -0
  100. package/dist/tools/result.d.ts.map +1 -0
  101. package/dist/tools/result.js +43 -0
  102. package/dist/tools/result.js.map +1 -0
  103. package/dist/tools/sheet-edit.d.ts +16 -0
  104. package/dist/tools/sheet-edit.d.ts.map +1 -0
  105. package/dist/tools/sheet-edit.js +325 -0
  106. package/dist/tools/sheet-edit.js.map +1 -0
  107. package/dist/tools/sheet-read.d.ts +10 -0
  108. package/dist/tools/sheet-read.d.ts.map +1 -0
  109. package/dist/tools/sheet-read.js +139 -0
  110. package/dist/tools/sheet-read.js.map +1 -0
  111. package/dist/tools/sheet-write.d.ts +15 -0
  112. package/dist/tools/sheet-write.d.ts.map +1 -0
  113. package/dist/tools/sheet-write.js +290 -0
  114. package/dist/tools/sheet-write.js.map +1 -0
  115. package/dist/tools/spreadsheet.d.ts +100 -0
  116. package/dist/tools/spreadsheet.d.ts.map +1 -0
  117. package/dist/tools/spreadsheet.js +210 -0
  118. package/dist/tools/spreadsheet.js.map +1 -0
  119. package/dist/tools/template.d.ts +18 -0
  120. package/dist/tools/template.d.ts.map +1 -0
  121. package/dist/tools/template.js +209 -0
  122. package/dist/tools/template.js.map +1 -0
  123. package/dist/tools/types.d.ts +79 -0
  124. package/dist/tools/types.d.ts.map +1 -0
  125. package/dist/tools/types.js +18 -0
  126. package/dist/tools/types.js.map +1 -0
  127. package/package.json +74 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `doc_paginate` — compute a Word document's real page layout.
3
+ *
4
+ * The capability nothing else here can substitute for. "How many pages is this?"
5
+ * and "which page is that heading on?" normally require Microsoft Word: the
6
+ * answer depends on line breaking, font metrics, widow/orphan control and table
7
+ * cell heights. This runs the layout engine and answers without Office
8
+ * installed, and without a rasteriser.
9
+ *
10
+ * It also updates computed field values — PAGE, NUMPAGES, PAGEREF, TOC entries —
11
+ * which otherwise only refresh when a human presses F9 in Word.
12
+ */
13
+ import { Io, Layout, Query } from "documonster/word";
14
+ import { z } from "zod";
15
+ import { toolError } from "../errors.js";
16
+ import { assertWritable, resolveEditTarget, resolveInRoot } from "../sandbox.js";
17
+ import { assertReadableSize, assertUnchanged, backupOnce, describeBackup, fingerprint, isSameFile, replaceAtomically, writeWithPolicy } from "./fs-helpers.js";
18
+ import { textResult } from "./result.js";
19
+ import { defineTool } from "./types.js";
20
+ /** Headings listed in one call. */
21
+ const MAX_HEADINGS = 150;
22
+ export const docPaginateTool = defineTool({
23
+ name: "doc_paginate",
24
+ group: "word",
25
+ title: "Compute a document's page layout",
26
+ description: "Compute how many pages a .docx really is, and which page each heading falls on, by running the layout engine — no Word installation needed. Optionally update computed fields (PAGE, NUMPAGES, PAGEREF) and regenerate the table of contents with correct page numbers, which otherwise only refresh when someone opens the document in Word.",
27
+ inputSchema: {
28
+ path: z.string().min(1).describe("Document path (.docx), relative to the server root."),
29
+ updateFields: z
30
+ .boolean()
31
+ .optional()
32
+ .describe("Recompute field values and the table of contents, then save. Requires a writable server. Defaults to false — pagination alone changes nothing on disk."),
33
+ out: z
34
+ .string()
35
+ .optional()
36
+ .describe("With updateFields, write below @output/. Required for input files unless --allow-in-place is enabled."),
37
+ overwrite: z.boolean().optional().describe("Replace an existing `out` file. Defaults to false.")
38
+ },
39
+ annotations: {
40
+ readOnlyHint: false,
41
+ destructiveHint: true,
42
+ idempotentHint: true,
43
+ openWorldHint: false
44
+ },
45
+ // Pagination alone writes nothing; only `updateFields` does. Declared
46
+ // non-mutating so page counts stay available on a read-only server, with
47
+ // assertWritable guarding the write path.
48
+ mutates: false,
49
+ handler: async (args, context) => {
50
+ const { config } = context;
51
+ const resolved = await resolveInRoot(config, args.path, { mustExist: true });
52
+ await assertReadableSize(config, resolved, args.path);
53
+ const inputVersion = await fingerprint(resolved);
54
+ const doc = await Io.readFile(resolved).catch((cause) => {
55
+ throw toolError.unsupported(`could not read ${args.path} as a Word document`, "Run doc_inspect to check the real file type.", { cause });
56
+ });
57
+ // Page size always comes from the document's own section properties:
58
+ // Layout.document takes no geometry override, and exposing a parameter the
59
+ // engine ignores would be worse than not having one.
60
+ const layout = Layout.document(doc);
61
+ const lines = [
62
+ `# ${args.path} — page layout`,
63
+ "",
64
+ `- **pages: ${layout.pageCount}**`,
65
+ `- sections: ${layout.sectionPageCounts.length} (${layout.sectionPageCounts.join(", ")} page(s) each)`,
66
+ `- paragraphs: ${Query.paragraphCount(doc)}`,
67
+ `- words: ${Query.countWords(doc)}`,
68
+ "- page size: taken from the document's own section setup"
69
+ ];
70
+ // Walk the body directly rather than using Query.getHeadings: that reports a
71
+ // walk-order ordinal which counts paragraphs nested in tables, whereas
72
+ // contentPages is indexed by body position. Mixing the two misreports pages
73
+ // for any document containing a table.
74
+ const headings = collectHeadings(doc, layout.contentPages);
75
+ if (headings.length > 0) {
76
+ lines.push("", "## Headings by page", "", "| page | level | heading |", "| --- | --- | --- |", ...headings
77
+ .slice(0, MAX_HEADINGS)
78
+ .map(heading => `| ${heading.page} | H${heading.level} | ${heading.text} |`));
79
+ if (headings.length > MAX_HEADINGS) {
80
+ lines.push("", `[${headings.length - MAX_HEADINGS} more not listed]`);
81
+ }
82
+ }
83
+ else {
84
+ lines.push("", "This document has no headings, so there is no per-section page map.");
85
+ }
86
+ if (layout.bookmarkPages.size > 0) {
87
+ lines.push("", `Bookmarks: ${[...layout.bookmarkPages.entries()]
88
+ .slice(0, 20)
89
+ .map(([name, page]) => `\`${name}\` → page ${page}`)
90
+ .join(", ")}`);
91
+ }
92
+ if (args.updateFields !== true) {
93
+ lines.push("", "Nothing was written. Pass `updateFields: true` to recompute PAGE / NUMPAGES / PAGEREF fields and the table of contents.");
94
+ return textResult(config, lines.join("\n"));
95
+ }
96
+ assertWritable(config);
97
+ const withFields = Io.updateFields(doc);
98
+ const withToc = Io.updateTableOfContents(withFields);
99
+ await assertUnchanged(resolved, inputVersion);
100
+ const writeTarget = await resolveEditTarget(config, args.path, args.out);
101
+ const inPlace = isSameFile(writeTarget.path, resolved);
102
+ const backupPath = inPlace ? await backupOnce(writeTarget) : undefined;
103
+ if (inPlace) {
104
+ await replaceAtomically(writeTarget.path, temporary => Io.writeFile(withToc, temporary));
105
+ }
106
+ else {
107
+ await writeWithPolicy(writeTarget.path, args.overwrite === true, temporary => Io.writeFile(withToc, temporary));
108
+ }
109
+ lines.push("", `Fields and table of contents recomputed and written to ${writeTarget.display}${inPlace ? " (in place)" : ""}.`, ...describeBackup(writeTarget.display, backupPath), "", "Read it back with doc_read to verify before reporting success to the user.");
110
+ return textResult(config, lines.join("\n"));
111
+ }
112
+ });
113
+ /** Headings in body order, each with the page its paragraph starts on. */
114
+ function collectHeadings(doc, contentPages) {
115
+ const found = [];
116
+ doc.body.forEach((item, index) => {
117
+ if (item.type !== "paragraph") {
118
+ return;
119
+ }
120
+ const style = item.properties?.style;
121
+ if (typeof style !== "string") {
122
+ return;
123
+ }
124
+ const match = /^Heading(\d)$/i.exec(style);
125
+ if (match === null) {
126
+ return;
127
+ }
128
+ const text = paragraphText(item);
129
+ if (text.length === 0) {
130
+ return;
131
+ }
132
+ found.push({
133
+ level: Number(match[1] ?? 1),
134
+ text: text.replace(/\|/g, "\\|").slice(0, 120),
135
+ page: contentPages[index] ?? 1
136
+ });
137
+ });
138
+ return found;
139
+ }
140
+ /** Concatenate a paragraph's text runs. */
141
+ function paragraphText(paragraph) {
142
+ let out = "";
143
+ for (const child of paragraph.children ?? []) {
144
+ const content = child.content;
145
+ for (const item of content ?? []) {
146
+ if (typeof item.text === "string") {
147
+ out += item.text;
148
+ }
149
+ }
150
+ }
151
+ return out.trim();
152
+ }
153
+ //# sourceMappingURL=doc-paginate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-paginate.js","sourceRoot":"","sources":["../../src/tools/doc-paginate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `doc_read` — read a document's text content.
3
+ *
4
+ * Covers Word, PDF, Markdown and plain text behind one tool because from the
5
+ * caller's point of view they are the same request: "what does this say". The
6
+ * differences that do matter are surfaced in the output — Word gains structure
7
+ * (headings, tables), PDF gains page boundaries — rather than in the schema.
8
+ *
9
+ * Word output is Markdown rather than flat text: headings, lists and tables
10
+ * survive, which is what lets a model answer questions about structure instead
11
+ * of only about words.
12
+ */
13
+ export declare const docReadTool: import("./types.js").AnyToolDefinition;
14
+ //# sourceMappingURL=doc-read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-read.d.ts","sourceRoot":"","sources":["../../src/tools/doc-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmBH,eAAO,MAAM,WAAW,wCA0DtB,CAAC"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `doc_read` — read a document's text content.
3
+ *
4
+ * Covers Word, PDF, Markdown and plain text behind one tool because from the
5
+ * caller's point of view they are the same request: "what does this say". The
6
+ * differences that do matter are surfaced in the output — Word gains structure
7
+ * (headings, tables), PDF gains page boundaries — rather than in the schema.
8
+ *
9
+ * Word output is Markdown rather than flat text: headings, lists and tables
10
+ * survive, which is what lets a model answer questions about structure instead
11
+ * of only about words.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { Pdf } from "documonster/pdf";
15
+ import { Io, Query } from "documonster/word";
16
+ import { renderToMarkdown } from "documonster/word/markdown";
17
+ import { z } from "zod";
18
+ import { toolError } from "../errors.js";
19
+ import { resolveInRoot } from "../sandbox.js";
20
+ import { continuationNote, parsePages, requireFormat, takeLines } from "./document.js";
21
+ import { assertReadableSize } from "./fs-helpers.js";
22
+ import { formatBytes, textResult } from "./result.js";
23
+ import { defineTool } from "./types.js";
24
+ /** Lines returned when the caller does not say. */
25
+ const DEFAULT_MAX_LINES = 200;
26
+ export const docReadTool = defineTool({
27
+ name: "doc_read",
28
+ group: ["word", "pdf"],
29
+ title: "Read a document's text",
30
+ description: "Read the text of a .docx, .pdf, .md or .txt file. Word documents come back as Markdown so headings, lists and tables survive; PDFs are reported page by page. Reads a bounded number of lines and always says what it omitted. Call doc_inspect first to learn the file's real type.",
31
+ inputSchema: {
32
+ path: z.string().min(1).describe("Document path, relative to the server root."),
33
+ pages: z
34
+ .union([z.array(z.number().int().positive()), z.string()])
35
+ .optional()
36
+ .describe('PDF only: pages to read, as [1,2,5] or "1-3,7". Omit for all pages.'),
37
+ maxLines: z
38
+ .number()
39
+ .int()
40
+ .positive()
41
+ .max(5000)
42
+ .optional()
43
+ .describe(`Line cap for this call. Defaults to ${DEFAULT_MAX_LINES}.`),
44
+ startLine: z
45
+ .number()
46
+ .int()
47
+ .positive()
48
+ .optional()
49
+ .describe("1-based line to resume from — use the value the previous call reported."),
50
+ outline: z
51
+ .boolean()
52
+ .optional()
53
+ .describe("Word only: return just the heading outline and document statistics instead of the body. Cheap way to decide what to read.")
54
+ },
55
+ annotations: { readOnlyHint: true, openWorldHint: false },
56
+ mutates: false,
57
+ handler: async (args, context) => {
58
+ const { config } = context;
59
+ const resolved = await resolveInRoot(config, args.path, { mustExist: true });
60
+ await assertReadableSize(config, resolved, args.path);
61
+ const format = requireFormat(args.path, "path");
62
+ switch (format) {
63
+ case "docx":
64
+ return textResult(config, await readWord(resolved, args));
65
+ case "pdf":
66
+ return textResult(config, await readPdf(resolved, args));
67
+ case "md":
68
+ case "txt":
69
+ case "html":
70
+ return textResult(config, await readPlainText(resolved, args, format));
71
+ default:
72
+ throw toolError.unsupported(`doc_read cannot read .${format} files`, format === "xlsx" || format === "csv"
73
+ ? "Use sheet_read for spreadsheets and CSV files."
74
+ : "Supported: .docx, .pdf, .md, .txt, .html");
75
+ }
76
+ }
77
+ });
78
+ async function readWord(resolved, args) {
79
+ const doc = await Io.readFile(resolved).catch((cause) => {
80
+ throw toolError.unsupported(`could not read ${args.path} as a Word document`, "If it is password-protected, that is not supported here yet. Run doc_inspect to confirm the container type — a CFB file is either a legacy .doc or an encrypted .docx.", { cause });
81
+ });
82
+ const headings = Query.getHeadings(doc);
83
+ const stats = [
84
+ `- paragraphs: ${Query.paragraphCount(doc)}`,
85
+ `- words: ${Query.countWords(doc)}`,
86
+ `- tables: ${Query.tableCount(doc)}`,
87
+ `- images: ${Query.listImages(doc).length}`
88
+ ];
89
+ if (args.outline === true) {
90
+ const lines = [`# ${args.path} — outline`, "", ...stats, ""];
91
+ if (headings.length === 0) {
92
+ lines.push("This document has no headings.", "", "Read the body with `outline: false`.");
93
+ return lines.join("\n");
94
+ }
95
+ lines.push("## Headings", "");
96
+ for (const heading of headings) {
97
+ lines.push(`${" ".repeat(Math.max(0, heading.level - 1))}- H${heading.level}: ${heading.text}`);
98
+ }
99
+ lines.push("", "Read the body by calling again without `outline`.");
100
+ return lines.join("\n");
101
+ }
102
+ const markdown = renderToMarkdown(doc);
103
+ const window = takeLines(markdown, args.startLine ?? 1, args.maxLines ?? DEFAULT_MAX_LINES);
104
+ return [
105
+ `# ${args.path} (Word, as Markdown)`,
106
+ "",
107
+ ...stats,
108
+ `- showing lines ${window.startLine}–${window.endLine} of ${window.totalLines}`,
109
+ ...(headings.length > 0
110
+ ? [`- headings: ${headings.length} (use \`outline: true\` for just the outline)`]
111
+ : []),
112
+ ...continuationNote(window),
113
+ "",
114
+ window.text
115
+ ].join("\n");
116
+ }
117
+ async function readPdf(resolved, args) {
118
+ const bytes = await readFile(resolved);
119
+ const wanted = parsePages(args.pages);
120
+ const result = await Pdf.read(new Uint8Array(bytes), {
121
+ extractText: true,
122
+ extractImages: false,
123
+ extractTables: false,
124
+ extractAnnotations: false,
125
+ extractFormFields: false,
126
+ extractBookmarks: true,
127
+ ...(wanted === undefined ? {} : { pages: wanted })
128
+ }).catch((cause) => {
129
+ throw toolError.unsupported(`could not read ${args.path} as a PDF`, "If it is password-protected, this tool cannot open it yet.", { cause });
130
+ });
131
+ const pageCount = result.metadata?.pageCount ?? result.pages.length;
132
+ const header = [
133
+ `# ${args.path} (PDF)`,
134
+ "",
135
+ `- pages in file: ${pageCount}`,
136
+ `- pages read: ${result.pages.length}${wanted === undefined ? "" : ` (requested ${wanted.join(", ")})`}`,
137
+ `- size: ${formatBytes(bytes.byteLength)}`
138
+ ];
139
+ if (result.metadata?.title !== undefined && result.metadata.title.length > 0) {
140
+ header.push(`- title: ${result.metadata.title}`);
141
+ }
142
+ if (result.bookmarks !== undefined && result.bookmarks.length > 0) {
143
+ header.push(`- bookmarks: ${result.bookmarks.length}`);
144
+ }
145
+ // Page markers are load-bearing: without them a model cannot cite "page 4",
146
+ // and a heading split across a page break reads as two unrelated fragments.
147
+ const body = result.pages
148
+ .map(page => `\n## Page ${page.pageNumber}\n\n${page.text.trim().length === 0 ? "_(no extractable text — this page may be a scan)_" : page.text}`)
149
+ .join("\n");
150
+ const window = takeLines(body, args.startLine ?? 1, args.maxLines ?? DEFAULT_MAX_LINES);
151
+ const emptyPages = result.pages.filter(page => page.text.trim().length === 0).length;
152
+ if (emptyPages > 0) {
153
+ header.push(`- **${emptyPages} page(s) yielded no text.** There is no OCR here, so a scanned page reads as empty — say so rather than inferring content.`);
154
+ }
155
+ const warnings = result.pages.flatMap(page => page.warnings ?? []);
156
+ if (warnings.length > 0) {
157
+ header.push(`- parser warnings: ${warnings.slice(0, 3).join("; ")}`);
158
+ }
159
+ header.push(`- showing lines ${window.startLine}–${window.endLine} of ${window.totalLines}`);
160
+ header.push(...continuationNote(window));
161
+ return [...header, window.text].join("\n");
162
+ }
163
+ async function readPlainText(resolved, args, format) {
164
+ const raw = await readFile(resolved, "utf8");
165
+ const window = takeLines(raw, args.startLine ?? 1, args.maxLines ?? DEFAULT_MAX_LINES);
166
+ return [
167
+ `# ${args.path} (${format})`,
168
+ "",
169
+ `- showing lines ${window.startLine}–${window.endLine} of ${window.totalLines}`,
170
+ ...continuationNote(window),
171
+ "",
172
+ window.text
173
+ ].join("\n");
174
+ }
175
+ //# sourceMappingURL=doc-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-read.js","sourceRoot":"","sources":["../../src/tools/doc-read.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `doc_review` — compare two document versions, or work through one document's
3
+ * tracked changes.
4
+ *
5
+ * The one capability that justified a new tool rather than an operation on an
6
+ * existing one, for two reasons: it takes *two* inputs, which no other tool's
7
+ * shape allows, and contract review is the document task where a model's help is
8
+ * worth most — reading a redline is tedious, and the consequences of missing a
9
+ * changed amount are real.
10
+ *
11
+ * Both modes answer the same question — "what changed" — so they share a tool.
12
+ * Accepting or rejecting is opt-in and never implicit: the summary is what the
13
+ * caller usually wants, and applying a revision is a decision a person should
14
+ * make.
15
+ */
16
+ export declare const docReviewTool: import("./types.js").AnyToolDefinition;
17
+ //# sourceMappingURL=doc-review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-review.d.ts","sourceRoot":"","sources":["../../src/tools/doc-review.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAwBH,eAAO,MAAM,aAAa,wCA4DxB,CAAC"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `doc_review` — compare two document versions, or work through one document's
3
+ * tracked changes.
4
+ *
5
+ * The one capability that justified a new tool rather than an operation on an
6
+ * existing one, for two reasons: it takes *two* inputs, which no other tool's
7
+ * shape allows, and contract review is the document task where a model's help is
8
+ * worth most — reading a redline is tedious, and the consequences of missing a
9
+ * changed amount are real.
10
+ *
11
+ * Both modes answer the same question — "what changed" — so they share a tool.
12
+ * Accepting or rejecting is opt-in and never implicit: the summary is what the
13
+ * caller usually wants, and applying a revision is a decision a person should
14
+ * make.
15
+ */
16
+ import { Diff, Io, Query } from "documonster/word";
17
+ import { z } from "zod";
18
+ import { toolError } from "../errors.js";
19
+ import { assertWritable, resolveEditTarget, resolveInRoot } from "../sandbox.js";
20
+ import { assertReadableSize, assertUnchanged, backupOnce, describeBackup, fingerprint, isSameFile, replaceAtomically, writeWithPolicy } from "./fs-helpers.js";
21
+ import { textResult } from "./result.js";
22
+ import { defineTool } from "./types.js";
23
+ /** Entries reported in one call. */
24
+ const MAX_ENTRIES = 120;
25
+ export const docReviewTool = defineTool({
26
+ name: "doc_review",
27
+ group: "word",
28
+ title: "Review document changes",
29
+ description: "Compare two .docx versions paragraph by paragraph, or list a document's tracked changes with their authors. Optionally accept or reject revisions. Give `path` and `against` to compare two files; give `path` alone to review tracked changes in one.",
30
+ inputSchema: {
31
+ path: z.string().min(1).describe("The document, or the earlier version when comparing."),
32
+ against: z
33
+ .string()
34
+ .optional()
35
+ .describe("The later version. Supply it to compare two files instead of reviewing revisions."),
36
+ apply: z
37
+ .enum(["accept-all", "reject-all"])
38
+ .optional()
39
+ .describe("Apply every tracked change. Omit to only report them — accepting a revision is a decision a person should usually make."),
40
+ out: z
41
+ .string()
42
+ .optional()
43
+ .describe("With `apply`, write below @output/. Required for input files unless --allow-in-place is enabled."),
44
+ backup: z
45
+ .boolean()
46
+ .optional()
47
+ .describe("When applying in place, copy the original to <name>.bak first. Defaults to true."),
48
+ overwrite: z.boolean().optional().describe("Replace an existing `out` file. Defaults to false.")
49
+ },
50
+ annotations: {
51
+ readOnlyHint: false,
52
+ // Conditional: listing/comparing is read-only, but `apply` can replace the
53
+ // input. MCP annotations cannot depend on arguments, so choose the safer
54
+ // hint and let clients ask for confirmation.
55
+ destructiveHint: true,
56
+ idempotentHint: true,
57
+ openWorldHint: false
58
+ },
59
+ // Comparing and listing are read-only; only `apply` writes. Declared
60
+ // non-mutating so a read-only server can still review changes, with
61
+ // assertWritable guarding the write path.
62
+ mutates: false,
63
+ handler: async (args, context) => {
64
+ const { config } = context;
65
+ if (args.against !== undefined) {
66
+ if (args.apply !== undefined) {
67
+ throw toolError.invalidInput("`apply` cannot be combined with `against`", "Comparing two files reports differences; there are no revisions in either to accept. Review one file's tracked changes instead.");
68
+ }
69
+ return textResult(config, await compare(config, args.path, args.against));
70
+ }
71
+ return textResult(config, await reviewRevisions(config, args));
72
+ }
73
+ });
74
+ async function compare(config, aPath, bPath) {
75
+ const [aResolved, bResolved] = await Promise.all([
76
+ resolveInRoot(config, aPath, { mustExist: true }),
77
+ resolveInRoot(config, bPath, { mustExist: true })
78
+ ]);
79
+ await Promise.all([
80
+ assertReadableSize(config, aResolved, aPath),
81
+ assertReadableSize(config, bResolved, bPath)
82
+ ]);
83
+ const [a, b] = await Promise.all([readWord(aResolved, aPath), readWord(bResolved, bPath)]);
84
+ const diff = Diff.documents(a, b);
85
+ if (diff.summary.added === 0 && diff.summary.deleted === 0 && diff.summary.modified === 0) {
86
+ return [
87
+ `# ${aPath} → ${bPath}`,
88
+ "",
89
+ "**No textual differences.** Every paragraph matches.",
90
+ "",
91
+ "This comparison covers body paragraphs only. Changes in headers, footers, footnotes, endnotes, comments, images and formatting are not detected — use doc_search with a `format` filter for appearance, and inspect those containers separately when they matter."
92
+ ].join("\n");
93
+ }
94
+ // Changed paragraphs first: a reviewer reading in document order wades through
95
+ // unchanged text to find the three lines that matter.
96
+ const changed = diff.entries.filter(entry => entry.type !== "unchanged");
97
+ const lines = [
98
+ `# ${aPath} → ${bPath}`,
99
+ "",
100
+ `- paragraphs compared: ${diff.summary.totalParagraphs}`,
101
+ `- **modified: ${diff.summary.modified} · added: ${diff.summary.added} · deleted: ${diff.summary.deleted}** · unchanged: ${diff.summary.unchanged}`,
102
+ ""
103
+ ];
104
+ for (const entry of changed.slice(0, MAX_ENTRIES)) {
105
+ const where = entry.newIndex !== undefined
106
+ ? `paragraph ${entry.newIndex + 1}`
107
+ : `was paragraph ${(entry.oldIndex ?? 0) + 1}`;
108
+ if (entry.type === "modified") {
109
+ lines.push(`## modified — ${where}`, "", `- before: ${quote(entry.oldText)}`, `- after: ${quote(entry.newText)}`, "");
110
+ }
111
+ else if (entry.type === "added") {
112
+ lines.push(`## added — ${where}`, "", `- ${quote(entry.newText)}`, "");
113
+ }
114
+ else {
115
+ lines.push(`## deleted — ${where}`, "", `- ${quote(entry.oldText)}`, "");
116
+ }
117
+ }
118
+ if (changed.length > MAX_ENTRIES) {
119
+ lines.push(`[${changed.length - MAX_ENTRIES} further change(s) not listed]`, "");
120
+ }
121
+ lines.push("This comparison covers body-paragraph text only. Headers, footers, footnotes, endnotes, comments, images and formatting are outside its scope.");
122
+ return lines.join("\n");
123
+ }
124
+ async function reviewRevisions(config, args) {
125
+ const resolved = await resolveInRoot(config, args.path, { mustExist: true });
126
+ await assertReadableSize(config, resolved, args.path);
127
+ const inputVersion = await fingerprint(resolved);
128
+ const doc = await readWord(resolved, args.path);
129
+ const revisions = Query.listRevisions(doc);
130
+ if (revisions.length === 0) {
131
+ return [
132
+ `# ${args.path}`,
133
+ "",
134
+ "This document contains no tracked changes.",
135
+ "",
136
+ "To compare it against another version, pass `against`."
137
+ ].join("\n");
138
+ }
139
+ // A revision's author and kind are optional in the model — Word permits an
140
+ // anonymous change — so both are normalised before counting.
141
+ const changes = collectRevisionText(doc);
142
+ const byAuthor = new Map();
143
+ const byType = new Map();
144
+ for (const revision of revisions) {
145
+ const author = revision.author ?? "(unattributed)";
146
+ const type = revision.type ?? "(unknown)";
147
+ byAuthor.set(author, (byAuthor.get(author) ?? 0) + 1);
148
+ byType.set(type, (byType.get(type) ?? 0) + 1);
149
+ }
150
+ const lines = [
151
+ `# ${args.path} — ${revisions.length} tracked change(s)`,
152
+ "",
153
+ `- by author: ${[...byAuthor].map(([author, count]) => `${author} (${count})`).join(", ")}`,
154
+ `- by kind: ${[...byType].map(([type, count]) => `${type} (${count})`).join(", ")}`,
155
+ "",
156
+ "| id | kind | author | text | date |",
157
+ "| --- | --- | --- | --- | --- |",
158
+ ...revisions.slice(0, MAX_ENTRIES).map(revision => {
159
+ const text = changes.get(revision.id);
160
+ return `| ${revision.id} | ${revision.type ?? "—"} | ${revision.author ?? "(unattributed)"} | ${text === undefined ? "—" : quote(text)} | ${revision.date ?? "—"} |`;
161
+ })
162
+ ];
163
+ if (revisions.length > MAX_ENTRIES) {
164
+ lines.push("", `[${revisions.length - MAX_ENTRIES} more not listed]`);
165
+ }
166
+ if (args.apply === undefined) {
167
+ lines.push("", 'Nothing was written. Pass `apply: "accept-all"` or `"reject-all"` to resolve them.', "", "Read the document with doc_read to see the text as it stands — inserted text is included, deleted text is not.");
168
+ return lines.join("\n");
169
+ }
170
+ assertWritable(config);
171
+ await assertUnchanged(resolved, inputVersion);
172
+ // Re-read: accept/reject mutate the document **in place** and return the
173
+ // number handled, so the listing's instance must not be reused.
174
+ const fresh = await readWord(resolved, args.path);
175
+ const handled = args.apply === "accept-all" ? Query.acceptAllRevisions(fresh) : Query.rejectAllRevisions(fresh);
176
+ const writeTarget = await resolveEditTarget(config, args.path, args.out);
177
+ const inPlace = isSameFile(writeTarget.path, resolved);
178
+ const backupPath = inPlace && (args.backup ?? true) ? await backupOnce(writeTarget) : undefined;
179
+ if (inPlace) {
180
+ await replaceAtomically(writeTarget.path, temporary => Io.writeFile(fresh, temporary));
181
+ }
182
+ else {
183
+ await writeWithPolicy(writeTarget.path, args.overwrite === true, temporary => Io.writeFile(fresh, temporary));
184
+ }
185
+ lines.push("", `**${args.apply === "accept-all" ? "Accepted" : "Rejected"} ${handled} revision(s)**, written to ${writeTarget.display}${inPlace ? " (in place)" : ""}.`, `- revisions remaining: ${countRevisions(fresh)}`, ...describeBackup(writeTarget.display, backupPath), "", "Read it back with doc_read to confirm the resulting text.");
186
+ return lines.join("\n");
187
+ }
188
+ /**
189
+ * Count remaining revisions, tolerating a failure to walk the document.
190
+ *
191
+ * Defensive on purpose: during development the document walker threw on one
192
+ * particular revision arrangement that could not afterwards be reproduced in
193
+ * isolation. Whatever the cause, a verification step must not be able to fail
194
+ * the write that already succeeded.
195
+ */
196
+ function countRevisions(doc) {
197
+ try {
198
+ return String(Query.listRevisions(doc).length);
199
+ }
200
+ catch {
201
+ return "could not be counted — read the document to confirm";
202
+ }
203
+ }
204
+ /**
205
+ * Text covered by each revision, keyed by revision id.
206
+ *
207
+ * `listRevisions` reports who changed what *kind* of thing but not the words,
208
+ * which makes a redline summary useless on its own — a reviewer cannot approve
209
+ * "insert by Alice". The document is walked here to recover the runs, guarded
210
+ * because a malformed revision arrangement has been seen to break the walker and
211
+ * a listing must not fail for want of an annotation.
212
+ */
213
+ function collectRevisionText(doc) {
214
+ const found = new Map();
215
+ const runText = (run) => (run.content ?? []).map(item => item.text ?? "").join("");
216
+ try {
217
+ Query.walkDocument(doc, {
218
+ visitInsertedRun(node) {
219
+ const id = node.revision?.id;
220
+ if (id !== undefined) {
221
+ found.set(id, runText(node.run));
222
+ }
223
+ },
224
+ visitDeletedRun(node) {
225
+ const id = node.revision?.id;
226
+ if (id !== undefined) {
227
+ found.set(id, runText(node.run));
228
+ }
229
+ }
230
+ });
231
+ }
232
+ catch {
233
+ // Annotation is best-effort; the id/kind/author listing stands without it.
234
+ }
235
+ return found;
236
+ }
237
+ function quote(text) {
238
+ if (text === undefined || text.length === 0) {
239
+ return "_(empty)_";
240
+ }
241
+ const collapsed = text.replace(/\s+/g, " ").trim();
242
+ return collapsed.length > 400 ? `${collapsed.slice(0, 400)}…` : collapsed;
243
+ }
244
+ async function readWord(resolved, displayPath) {
245
+ return await Io.readFile(resolved).catch((cause) => {
246
+ throw toolError.unsupported(`could not read ${displayPath} as a Word document`, "doc_review works with .docx only. Run doc_inspect to check the real file type.", { cause });
247
+ });
248
+ }
249
+ //# sourceMappingURL=doc-review.js.map