stetdoc for Node and Bun
Opens the app with a prompt to read this page.
stetdoc turns a .docx into a working copy that an agent can read and edit, and compiles the edits back into the same
package as real Word tracked changes. Everything the edits did not touch is kept byte for byte. This package is the
library: the stetdoc engine as a native addon, with TypeScript types generated from the engine's own definitions.
import { Document, compare } from "@clauseo/stetdoc";
// bytes is a Uint8Array of a .docx.
const doc = Document.open(bytes);
// The body, or its outline when it is long.
const view = doc.view();
// Tracked by default.
const receipt = doc.edit({
base: doc.base(),
ops: [{ op: "replace", ref: "p4", find: "thirty", with: "sixty" }],
author: "Jane Doe",
date: "2026-10-06T10:00:00",
});
// Unedited, it returns the input bytes.
const { bytes: saved } = doc.save();
// Keep it between turns. Refs such as p4 stay valid in the resumed document.
const copy = doc.export();
const again = Document.resume(copy);
const { document: redline, receipt: diff } = compare(original, revised, { stamp: { author: "Jane Doe", date } });Requirements
Install it with npm install @clauseo/stetdoc or bun add @clauseo/stetdoc.
Node 22.18 or later, or Bun 1.3.13 or later, on one of seven targets: linux-x64-gnu, linux-arm64-gnu,
linux-x64-musl, linux-arm64-musl, darwin-x64, darwin-arm64 and win32-x64-msvc. The addon for each target is its
own package, @clauseo/stetdoc-<target>, which the package manager installs as an optional dependency. No package runs a script
on install. A missing addon throws an error that names the target and the paths it tried.
The rules
- Calls are synchronous: bytes in, bytes out. A worst-case call on a 300-page document takes about a second, so a server that cares about its event loop makes its calls from a worker thread. The addon loads in Node and Bun worker threads.
- One
Documentper document, on one thread. ADocumentbelongs to the thread that made it. Serialize the edits to it, and keep it (or itsWorkingCopy) between turns. Reopening saved bytes gives new refs, so an agent's refs would go stale. - No file I/O and no clock. The caller supplies the bytes, the author and the date (
YYYY-MM-DDTHH:MM:SS, the user's wall clock). - Offsets count UTF-16 code units, as a JavaScript string does, unless an edit says
offsets: "scalar". - Every failure is a thrown
StetdocError, except intextEditor, whose failures are text for the model.
Document
| Method | Returns | What it does |
|---|---|---|
Document.open(bytes, options?) | Document | Opens a package. options.limits bounds untrusted input; options.format is the working-copy format (stet/1 or json/1) |
Document.resume(workingCopy, options?) | Document | Resumes a working copy that export returned, with every ref still valid |
base() | string | The base token that a batch names |
view(spec?) | View | A page within a token budget: { scope, mode, budget, after, format, notes }, every field optional. { budget: 0, mode: "final", format: "json/1" } is the whole Final text as JSON |
edit(request) | Receipt | Applies ops (with base), a batch string in JSON or block text, or hand-edited files, signed by author and date. Options: untracked, scope (refs and ranges such as p40..p60), dryRun, quotes ("match" or "as_typed"), offsets |
save() | { bytes, receipt } | The package and a summary of it. An unedited document returns its input bytes exactly |
inspect(target) | Inspection | One block (p42, t3, r17), or styles, numbering or sections |
revisions(filter?) | RevisionInfo[] | The tracked changes, filtered by author and kind |
occurrences(text, scope?) | Occurrences | Every place text is, matched as an edit's find matches it, each with its block, clause and occurrence. scope is "auto" (the default), { range: { from, to } }, { clause: "11.1" }, { clause: ["11.1", "12.3"] } or { story: "body" } |
lookup(query) | Lookup | A defined term, a clause number or a cross-reference, resolved |
check() | CheckReport | Schema and Word-rule problems of the document as it is |
checkPending(request?) | CheckResult | A dry run of pending readable files, the revisions already there and the check report |
files() | ReadableFiles | The readable files, as export would write them |
export() | WorkingCopy | { files: [name, text][], complement: [name, Uint8Array][] }: plain data to store |
textEditor(input, options) | TextEditorResult | One call of Claude's text editor tool, below |
The operations, their fields and the error codes are listed by capabilities(), and every data type is in types.d.ts.
Claude's text editor tool
textEditor is the backend of Anthropic's text_editor_20250728 tool (tool name str_replace_based_edit_tool) over the
document's readable files. Pass the tool_use block's input exactly as the model sent it, as an object or its JSON:
const result = doc.textEditor(toolUse.input, { author: "Jane Doe", date, pathPrefix: "/documents/acme/" });
// { content: string, isError: boolean, committed: boolean, receipt?: Receipt }
return { type: "tool_result", tool_use_id: toolUse.id, content: result.content, is_error: result.isError };viewshows a file with line numbers, or with the prefix (or/) lists the files.view_rangeis supported.str_replaceneedsold_strto match exactly once. Otherwise the result isnot_found, orambiguous_matchwith each match and a line of context on each side.str_replaceandinsertcommit at once as one tracked edit, and show the touched blocks where they now are.createandundo_editareunsupported. A path outsidepathPrefixisnot_found.- A failure changes nothing, and comes back as text with
isErrorset, never as a thrown error.
Errors
try {
doc.edit(request);
} catch (error) {
if (error instanceof StetdocError) {
// A code such as "stale_ref" or "ambiguous_match". Codes are frozen from 1.0.
error.code;
// One line.
error.message;
// What to do about it.
error.next;
// The failing operation of a batch, when there is one.
error.opIndex;
// The readable file and the line of a failing edit of files, when there is one.
error.file;
error.line;
}
}JSON.stringify(error) gives the wire shape that the CLI's --json prints. The codes, their meanings and their exit
classes are in the repository's error reference, and capabilities().error_codes
lists them at run time.
Versions
version is the release the addon was built from, to pin against the CLI's stetdoc --version. formatVersion is the
version of the working copy's complement that this release writes and reads.
Licence
MIT or Apache-2.0, at your option.