Revise CLI menu

Library

ReviseDocument: open, read, edit, review, and export documents from Node.

npm i revise
import { 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
authorShown 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)
documentIdA stable id, for tool sessions and collaboration
imagesHow 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: number

A 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 throws section_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
bytesThe file
mode"review" or "final", as written (only .docx holds tracked changes; other formats are always final)
view"tracked", "accepted", or "original", as written
pendingTrackedChangesTracked changes still in the output
fidelityFor .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
warningsAnything 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.ydocThe 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"); // Uint8Array

The 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.replacements

Errors#

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 }.