@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.
- package/LICENSE +205 -0
- package/README.md +253 -0
- package/dist/capabilities.d.ts +29 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +188 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +64 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +189 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +66 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +96 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox.d.ts +86 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +224 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +66 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/archive-read.d.ts +31 -0
- package/dist/tools/archive-read.d.ts.map +1 -0
- package/dist/tools/archive-read.js +315 -0
- package/dist/tools/archive-read.js.map +1 -0
- package/dist/tools/archive-write.d.ts +9 -0
- package/dist/tools/archive-write.d.ts.map +1 -0
- package/dist/tools/archive-write.js +219 -0
- package/dist/tools/archive-write.js.map +1 -0
- package/dist/tools/chart.d.ts +93 -0
- package/dist/tools/chart.d.ts.map +1 -0
- package/dist/tools/chart.js +263 -0
- package/dist/tools/chart.js.map +1 -0
- package/dist/tools/doc-convert.d.ts +13 -0
- package/dist/tools/doc-convert.d.ts.map +1 -0
- package/dist/tools/doc-convert.js +209 -0
- package/dist/tools/doc-convert.js.map +1 -0
- package/dist/tools/doc-paginate.d.ts +14 -0
- package/dist/tools/doc-paginate.d.ts.map +1 -0
- package/dist/tools/doc-paginate.js +153 -0
- package/dist/tools/doc-paginate.js.map +1 -0
- package/dist/tools/doc-read.d.ts +14 -0
- package/dist/tools/doc-read.d.ts.map +1 -0
- package/dist/tools/doc-read.js +175 -0
- package/dist/tools/doc-read.js.map +1 -0
- package/dist/tools/doc-review.d.ts +17 -0
- package/dist/tools/doc-review.d.ts.map +1 -0
- package/dist/tools/doc-review.js +249 -0
- package/dist/tools/doc-review.js.map +1 -0
- package/dist/tools/doc-search.d.ts +20 -0
- package/dist/tools/doc-search.d.ts.map +1 -0
- package/dist/tools/doc-search.js +268 -0
- package/dist/tools/doc-search.js.map +1 -0
- package/dist/tools/doc-write.d.ts +11 -0
- package/dist/tools/doc-write.d.ts.map +1 -0
- package/dist/tools/doc-write.js +81 -0
- package/dist/tools/doc-write.js.map +1 -0
- package/dist/tools/document.d.ts +74 -0
- package/dist/tools/document.d.ts.map +1 -0
- package/dist/tools/document.js +157 -0
- package/dist/tools/document.js.map +1 -0
- package/dist/tools/form-fill.d.ts +20 -0
- package/dist/tools/form-fill.d.ts.map +1 -0
- package/dist/tools/form-fill.js +270 -0
- package/dist/tools/form-fill.js.map +1 -0
- package/dist/tools/formula-evaluate.d.ts +14 -0
- package/dist/tools/formula-evaluate.d.ts.map +1 -0
- package/dist/tools/formula-evaluate.js +192 -0
- package/dist/tools/formula-evaluate.js.map +1 -0
- package/dist/tools/fs-helpers.d.ts +106 -0
- package/dist/tools/fs-helpers.d.ts.map +1 -0
- package/dist/tools/fs-helpers.js +257 -0
- package/dist/tools/fs-helpers.js.map +1 -0
- package/dist/tools/help.d.ts +227 -0
- package/dist/tools/help.d.ts.map +1 -0
- package/dist/tools/help.js +262 -0
- package/dist/tools/help.js.map +1 -0
- package/dist/tools/index.d.ts +33 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +78 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/inspect.d.ts +14 -0
- package/dist/tools/inspect.d.ts.map +1 -0
- package/dist/tools/inspect.js +463 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/pdf-edit.d.ts +26 -0
- package/dist/tools/pdf-edit.d.ts.map +1 -0
- package/dist/tools/pdf-edit.js +390 -0
- package/dist/tools/pdf-edit.js.map +1 -0
- package/dist/tools/result.d.ts +25 -0
- package/dist/tools/result.d.ts.map +1 -0
- package/dist/tools/result.js +43 -0
- package/dist/tools/result.js.map +1 -0
- package/dist/tools/sheet-edit.d.ts +16 -0
- package/dist/tools/sheet-edit.d.ts.map +1 -0
- package/dist/tools/sheet-edit.js +325 -0
- package/dist/tools/sheet-edit.js.map +1 -0
- package/dist/tools/sheet-read.d.ts +10 -0
- package/dist/tools/sheet-read.d.ts.map +1 -0
- package/dist/tools/sheet-read.js +139 -0
- package/dist/tools/sheet-read.js.map +1 -0
- package/dist/tools/sheet-write.d.ts +15 -0
- package/dist/tools/sheet-write.d.ts.map +1 -0
- package/dist/tools/sheet-write.js +290 -0
- package/dist/tools/sheet-write.js.map +1 -0
- package/dist/tools/spreadsheet.d.ts +100 -0
- package/dist/tools/spreadsheet.d.ts.map +1 -0
- package/dist/tools/spreadsheet.js +210 -0
- package/dist/tools/spreadsheet.js.map +1 -0
- package/dist/tools/template.d.ts +18 -0
- package/dist/tools/template.d.ts.map +1 -0
- package/dist/tools/template.js +209 -0
- package/dist/tools/template.js.map +1 -0
- package/dist/tools/types.d.ts +79 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +18 -0
- package/dist/tools/types.js.map +1 -0
- 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
|