Revise CLI menu
Library
ReviseDocument: open, read, edit, review, and export documents from Node.
npm i reviseimport { writeFile } from "node:fs/promises";
import { ReviseDocument } from "revise";
const doc = await ReviseDocument.open("contract.docx", { author: "Legal bot" });
await doc.replaceAll("Provider", "Northstar"); // tracked changes
await doc.reject({ section: "8" });
const { bytes, fidelity } = await doc.export({ format: "docx" });
await writeFile("contract-out.docx", bytes);
doc.dispose();The revise CLI is built on this API. ESM only; Node 20 or newer.
Opening a document#
ReviseDocument.open(source, options?) | A file path, or { bytes, filename }: .docx, .odt, .rtf, .md, .html, or .txt. A Word file keeps its original package, so untouched content exports byte for byte. |
ReviseDocument.create({ html?, markdown?, title? }, options?) | A new document from the HTML the tools read and write, or from Markdown |
ReviseDocument.restore(snapshot, options?) | From doc.snapshot(), or a bare Y.js update |
ReviseDocument.fromYDoc(ydoc, options?) | Wrap a Y.Doc you own, such as a collaboration room. The Y.Doc stays yours. |
Options:
| Option | |
|---|---|
author | Shown as the author of tracked changes and comments |
mode | "suggesting" records edits as tracked changes (the default for Word files); "editing" applies them directly (the default for plain text) |
documentId | A stable id, for tool sessions and collaboration |
images | How images inserted by URL are fetched. By default only public http(s) hosts are reachable. |
A document runs its operations one at a time, in the order you call them, so
concurrent calls are safe. Call dispose() when you're done.
Reading#
const status = await doc.status(); // blocks, words, pendingSuggestions, fidelity
const outline = await doc.outline(); // sections with block ranges and word counts
const summary = await doc.summary(); // page setup, headers and footers, headings, tables
const hits = doc.grep("indemnif", { ignoreCase: true }); // [{ id, blockIndex, count, text, matches }]Pending tracked changes read in a view: "tracked" (the default for
grep) finds text on either side of a change, writes the block as
"Fees are $[-100-]{+120+} per hour.", and labels each match "deleted",
"inserted", or null; "accepted" and "original" read the text as if every
change were accepted or rejected. The read tools return the accepted view;
doc.viewHtml(html, "tracked") marks their HTML with <ins>/<del> (each
with data-change-id and data-author) and "original" shows the text before
the changes.
status().fidelity says how faithfully the document will export back to its
original Word file.
Editing#
replaceAll(find, replace, options?) replaces every occurrence: in the
body, tables, and lists, in page headers and footers, and in footnotes and
endnotes. Curly and straight quotes, no-break and plain spaces, and
no-break and plain hyphens match each other, so '"Provider"' finds
“Provider”; a straight quote in the replacement then takes the document's
curly style. Options: ignoreCase, exact (characters must match exactly),
blocks (only these block ids). It returns replaced (occurrences),
blocks (ids of the blocks that contained the text), regions
(occurrences per part: body, header, footer, footnote, endnote),
changeIds (tracked changes created), failures (each: blockId, code,
message), and nearMisses: text it left alone that ignoreCase or
dropping exact would have matched (each: text, count, reason).
replaceText is its 3.0 name and still works. For everything else, run the
editing tools:
const result = await doc.tools.call("replace", {
id: hits[0].id,
replacements: [{ find: "thirty", replace: "sixty" }],
});
if (!result.ok) console.error(result.error.code, result.error.message);doc.tools.definitions() | Every tool's name, description, and JSON input schema |
doc.tools.call(name, input, { directMode? }) | Run a tool with typed input. Literal names infer the input and result types. |
doc.tools.run(name, input, { direct? }) | Run a tool by name with untyped input, as a model sends it |
Neither throws for a bad edit: both return { ok: true, value } or
{ ok: false, error }. A mutation's value.suggestionIds lists the tracked
changes it created. The tools are described in the tool
reference; revise runs the server subset (no
get_selection or view_image, no document_id).
await doc.setMode("editing") switches to direct edits; "suggesting"
switches back.
Review#
const changes = await doc.changes(); // [{ id, type, author, section, deleted, inserted }]
const inSection8 = await doc.changes({ section: "8" });
const preview = await doc.preview({ exceptSection: "Fees" }); // what a selection would settle
const accepted = await doc.accept({ ids: [changes[0].id] });
const rejected = await doc.reject({ all: true });
// { resolved, missing, unresolved }
const diff = await doc.diff(); // text: "~ a [-old-]{+new+} b", changed: numberA selection is exactly one of ids, all, or filters, which combine:
section: a heading and its subsections, by number ("8","8.1", Word's automatic numbering included) or title. A title is matched in full, or by text in only one heading; otherwise it throwssection_ambiguous.exceptSection: everything outside one.blocks: block ids and{ from, to }ranges in document order. Ids of list items, table cells, and blocks inside them work.
A section or block that doesn't exist throws (section_not_found,
block_not_found) rather than selecting nothing.
Output#
const docx = await doc.export({ format: "docx" }); // review: keeps tracked changes
const clean = await doc.export({ format: "docx", view: "accepted" }); // or mode: "final"
const before = await doc.export({ format: "docx", view: "original" }); // every change rejected
const pdf = await doc.export({ format: "pdf" });Formats: docx, pdf, markdown, html, txt. The result:
| Field | |
|---|---|
bytes | The file |
mode | "review" or "final", as written (only .docx holds tracked changes; other formats are always final) |
view | "tracked", "accepted", or "original", as written |
pendingTrackedChanges | Tracked changes still in the output |
fidelity | For .docx: method: "graft" carried untouched content over from the original byte for byte (exact when nothing changed); "regenerated" wrote the file fresh, with fallbackCode saying why |
warnings | Anything the output can't hold |
doc.renderPages({ startPage, pageCount, view }) draws pages as one PNG,
laid out the way the editor lays them out. It needs a canvas package:
npm i @napi-rs/canvas (or canvas).
State#
doc.snapshot() | The whole document, versioned, including the original Word package: for storing and restoring with revise. It is not a Y.js update. |
ReviseDocument.restore(snapshot) | Back from a snapshot |
doc.version() | An opaque value that changes whenever the document does |
doc.reload(bytes, filename) | Re-read a file that changed elsewhere. Block ids carry over only where identity is provable; changedIds lists kept ids whose content changed. |
doc.ydoc | The live Y.Doc. To hand the document to @reviseio/editor, send Y.encodeStateAsUpdate(doc.ydoc) and apply it with Y.applyUpdate on the editor's side (or share the Y.Doc over your collaboration provider). |
doc.resetToolContext() | Forget what an agent has read (after compacting its transcript) |
Comparing and converting#
import { compare, convert } from "revise";
const redline = await compare("v1.docx", "v2.docx");
// redline.document: the revised version with every difference tracked
// redline.verified: { acceptAllGivesRevised, rejectAllGivesOriginal }
const { bytes } = await redline.document.export({ format: "docx" });
const markdown = await convert("brief.docx", "markdown"); // Uint8ArrayThe raw package#
For what the tools don't cover, read and patch the .docx parts directly:
import { readFile } from "node:fs/promises";
import { catPart, listParts, patchPackage } from "revise";
const bytes = new Uint8Array(await readFile("contract.docx"));
const parts = await listParts(bytes); // [{ path, bytes }]
const styles = await catPart(bytes, "word/styles.xml", 'w:styleId="Heading2"');
const patched = await patchPackage(
bytes,
{ part: "word/settings.xml", find: "<w:zoom w:percent=\"100\"/>", replace: "<w:zoom w:percent=\"120\"/>" },
(path) => "",
);
// patched.bytes, patched.replacementsErrors#
Expected failures (a missing file, an unsupported format, a malformed patch)
throw ReviseError, with a stable machine-readable code. Tool calls never
throw for a bad edit; they return { ok: false, error }.