Agent Framework

Headless DOCX Agent API (Node)

DocxReviewer parses DOCX in Node, lets agents comment, propose tracked changes, batch LLM output, export .docx. Server-side, no DOM.

The headless transport for @eigenpal/docx-editor-agents. DocxReviewer parses a DOCX buffer, lets you comment / suggest tracked changes / accept / reject programmatically, and serializes back to .docx. No DOM, no React, runs on Node, edge, or any JavaScript runtime.

npm install @eigenpal/docx-editor-agents

DocxReviewer addresses paragraphs by index; the agent bridge wraps it with paraId. DocxReviewer is the low-level primitive: one process, one buffer, one batch. Indices map directly to the parsed Document.body.paragraphs[i] and don't drift because the buffer doesn't change under you. The agent toolkit speaks one tool catalog across transports though, so createReviewerBridge(reviewer) exposes the same paraId-based EditorBridge the live editor exposes (resolving paraId to index at call time). Use indices when your code drives the reviewer; wrap with the bridge when an LLM does.

Quick start

Parse a DOCX, mutate it, write it back. The DocxReviewer itself is index-addressed:

import { DocxReviewer } from '@eigenpal/docx-editor-agents';
 
const reviewer = await DocxReviewer.fromBuffer(buffer, 'AI Reviewer');
 
// Read: plain text with paragraph indices for LLM prompts
const text = reviewer.getContentAsText();
// [0] Introduction
// [1] The liability cap is $50k per incident.
// [2] (table, row 1, col 1) Revenue ...
 
// Comment on a paragraph (anchors to whole paragraph)
reviewer.addComment(1, 'This cap seems too low.');
 
// Replace text (creates a tracked change)
reviewer.replace(1, '$50k', '$500k');
 
// Or batch from an LLM JSON response
reviewer.applyReview({
  comments: [{ paragraphIndex: 1, text: 'Too low.' }],
  proposals: [{ paragraphIndex: 1, search: '$50k', replaceWith: '$500k' }],
});
 
// Export back to .docx
const output = await reviewer.toBuffer();

Wrap with createReviewerBridge for agent use

When an LLM drives the reviewer, wrap it. createReviewerBridge(reviewer) returns an EditorBridge whose methods take paraId instead of paragraphIndex, the same surface the live editor exposes, so one tool catalog, one set of prompts, both transports.

import { DocxReviewer, createReviewerBridge } from '@eigenpal/docx-editor-agents';
 
const reviewer = await DocxReviewer.fromBuffer(buffer, 'AI Reviewer');
const bridge = createReviewerBridge(reviewer);
 
const text = bridge.getContentAsText();
// [4F2A0001] Introduction
// [4F2A0002] The liability cap is $50k per incident.
 
bridge.addComment({ paraId: '4F2A0002', text: 'Too low.', search: '$50k' });
bridge.proposeChange({ paraId: '4F2A0002', search: '$50k', replaceWith: '$500k' });
 
const output = await reviewer.toBuffer();

The bridge resolves paraId to paragraphIndex internally on each call, so you keep the static-buffer guarantees while the agent talks paraIds.

DocxReviewer methods

Author is set once via DocxReviewer.fromBuffer(buffer, 'Author Name'). No need to repeat it on every call.

Read

MethodDescription
DocxReviewer.fromBuffer(buffer: ArrayBuffer, author?: string): Promise<DocxReviewer>Create a reviewer from a DOCX file buffer.
getContent(options?: GetContentOptions): ContentBlock[]Get document content as structured blocks (headings, paragraphs, tables, lists).
getContentAsText(options?: GetContentOptions): stringGet document content as plain text for LLM prompts. Each paragraph is prefixed with its index: [0] text, [1] text, etc. Table cells include position: [5] (table, row 1, col 2) cell text. Avoids JSON quote-escaping issues — LLMs can copy text verbatim.

Discover

MethodDescription
getChanges(filter?: ChangeFilter): ReviewChange[]Get all tracked changes in the document.
getComments(filter?: CommentFilter): ReviewComment[]Get all comments with their replies.

Comment

MethodDescription
addComment(paragraphIndex: number, text: string): numberAdd a comment on a paragraph.
addComment(options: AddCommentOptions): number(overload)
replyTo(commentId: number, text: string): numberReply to an existing comment.
replyTo(commentId: number, options: ReplyOptions): number(overload)
removeComment(commentId: number): voidRemove a comment by ID. Removing a top-level comment also removes its replies and the anchored range markers. Removing a reply only removes that reply.

Propose Changes

MethodDescription
replace(paragraphIndex: number, search: string, replaceWith: string): voidReplace text in a paragraph. Creates a tracked change (deletion + insertion).
replace(options: ProposeReplacementOptions): void(overload)

Resolve

MethodDescription
acceptChange(id: number): voidAccept a tracked change by its revision ID.
rejectChange(id: number): voidReject a tracked change by its revision ID.
acceptAll(): numberAccept all tracked changes. Returns count accepted.
rejectAll(): numberReject all tracked changes. Returns count rejected.

Batch

MethodDescription
applyReview(ops: BatchReviewOptions): BatchResultApply multiple review operations in one call. Uses the reviewer's default author. Individual failures are collected, not thrown.

Export

MethodDescription
toDocument(): DocumentGet the modified Document model.
toBuffer(): Promise<ArrayBuffer>Serialize back to a DOCX buffer. Requires the original buffer.

Types

GetContentOptions

FieldTypeDescription
fromIndex?number
toIndex?number
includeTrackedChanges?booleanAnnotate tracked changes inline. Default: true
includeCommentAnchors?booleanAnnotate comments inline. Default: true

BatchReviewOptions

FieldTypeDescription
accept?number[]
reject?number[]
comments?AddCommentOptions[]
replies?(ReplyOptions & \{ commentId: number \})[]
proposals?ProposeReplacementOptions[]

BatchResult

FieldTypeDescription
acceptednumber
rejectednumber
commentsAddednumber
repliesAddednumber
proposalsAddednumber
errorsBatchError[]

BatchError

FieldTypeDescription
operationstring
id?number
search?string
errorstring

ReviewChange

FieldTypeDescription
idnumber
type'insertion' | 'deletion' | 'moveFrom' | 'moveTo'
authorstring
datestring | null
textstring
contextstring
paragraphIndexnumber

ReviewComment

FieldTypeDescription
idnumber
authorstring
datestring | null
textstring
anchoredTextstring
paragraphIndexnumber
repliesReviewCommentReply[]
doneboolean

AddCommentOptions

FieldTypeDescription
paragraphIndexnumber
textstring
author?string
search?stringOptional: anchor to specific text. Omit to anchor whole paragraph.

ReplyOptions

FieldTypeDescription
textstring
author?string

ProposeReplacementOptions

FieldTypeDescription
paragraphIndexnumber
searchstring
replaceWithstring
author?string

Examples

Next.js API route

Upload a DOCX, send it to an LLM for review, apply comments and tracked changes, return the modified file.

// app/api/review/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
import { DocxReviewer } from '@eigenpal/docx-editor-agents';
 
const openai = new OpenAI();
 
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get('file') as File;
  if (!file) return NextResponse.json({ error: 'No file' }, { status: 400 });
 
  const reviewer = await DocxReviewer.fromBuffer(await file.arrayBuffer(), 'AI Reviewer');
 
  const response = await openai.chat.completions.create({
    model: 'gpt-5.4-mini',
    response_format: { type: 'json_object' },
    messages: [
      {
        role: 'system',
        content: `Review this document. Return JSON:
{
  "comments": [{ "paragraphIndex": <number>, "text": "<feedback>" }],
  "replacements": [{ "paragraphIndex": <number>, "search": "<phrase>", "replaceWith": "<better>" }]
}
paragraphIndex must match a [number] from the document.`,
      },
      { role: 'user', content: reviewer.getContentAsText() },
    ],
  });
 
  const actions = JSON.parse(response.choices[0]?.message?.content || '{}');
  reviewer.applyReview({ comments: actions.comments, proposals: actions.replacements });
 
  const output = await reviewer.toBuffer();
  return new NextResponse(output, {
    headers: {
      'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    },
  });
}

Node.js batch script

import { readFileSync, writeFileSync } from 'fs';
import { DocxReviewer } from '@eigenpal/docx-editor-agents';
 
const buffer = readFileSync('contract.docx');
const reviewer = await DocxReviewer.fromBuffer(buffer.buffer, 'Legal Bot');
 
reviewer.addComment(1, 'Liability cap is below industry standard.');
reviewer.replace(1, '$50k', '$500k');
 
const output = await reviewer.toBuffer();
writeFileSync('contract-reviewed.docx', Buffer.from(output));

Using with Claude (Anthropic)

import Anthropic from '@anthropic-ai/sdk';
import { DocxReviewer } from '@eigenpal/docx-editor-agents';
 
const client = new Anthropic();
const reviewer = await DocxReviewer.fromBuffer(buffer, 'Claude Reviewer');
 
const response = await client.messages.create({
  model: 'claude-sonnet-4-7',
  max_tokens: 4096,
  messages: [{
    role: 'user',
    content: `Review this document and return JSON with comments and replacements:\n\n${reviewer.getContentAsText()}`,
  }],
});
 
const actions = JSON.parse(response.content[0].text);
reviewer.applyReview({ comments: actions.comments, proposals: actions.replacements });

Runnable demos:


Source: @eigenpal/docx-editor-agents