@eigenpal/docx-editor-agents — headless, Word-like API for AI document review.
npm install @eigenpal/docx-editor-agents
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] (h1) 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 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
const output = await reviewer. toBuffer ();
Author is set once via DocxReviewer.fromBuffer(buffer, 'Author Name') — no need to repeat it on every call.
Method Description DocxReviewer.fromBuffer(buffer: ArrayBuffer, author?: string | undefined): Promise<DocxReviewer>Create a reviewer from a DOCX file buffer. getContent(options?: GetContentOptions | undefined): ContentBlock[]Get document content as structured blocks (headings, paragraphs, tables, lists). getContentAsText(options?: GetContentOptions | undefined): 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.
Method Description getChanges(filter?: ChangeFilter | undefined): ReviewChange[]Get all tracked changes in the document. getComments(filter?: CommentFilter | undefined): ReviewComment[]Get all comments with their replies.
Method Description 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)
Method Description replace(paragraphIndex: number, search: string, replaceWith: string): voidReplace text in a paragraph. Creates a tracked change (deletion + insertion). replace(options: ProposeReplacementOptions): void(overload) proposeInsertion(options: ProposeInsertionOptions): voidInsert text as a tracked change. proposeDeletion(options: ProposeDeletionOptions): voidDelete text as a tracked change.
Method Description 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.
Method Description applyReview(ops: BatchReviewOptions): BatchResultApply multiple review operations in one call. Uses the reviewer's default author. Individual failures are collected, not thrown.
Method Description toDocument(): DocumentGet the modified Document model. toBuffer(): Promise<ArrayBuffer>Serialize back to a DOCX buffer. Requires the original buffer.
Field Type Description fromIndex?numbertoIndex?numberincludeTrackedChanges?booleanAnnotate tracked changes inline. Default: true includeCommentAnchors?booleanAnnotate comments inline. Default: true
Field Type Description accept?number[]reject?number[]comments?AddCommentOptions[]replies?(ReplyOptions & \{ commentId: number; \})[]proposals?ProposeReplacementOptions[]
Field Type Description acceptednumberrejectednumbercommentsAddednumberrepliesAddednumberproposalsAddednumbererrorsBatchError[]
Field Type Description operationstringid?numbersearch?stringerrorstring
Field Type Description idnumbertype'insertion' | 'deletion' | 'moveFrom' | 'moveTo'authorstringdatestring | nulltextstringcontextstringparagraphIndexnumber
Field Type Description idnumberauthorstringdatestring | nulltextstringanchoredTextstringparagraphIndexnumberrepliesReviewCommentReply[]doneboolean
Field Type Description paragraphIndexnumbertextstringauthor?stringsearch?stringOptional: anchor to specific text. Omit to anchor whole paragraph.
Field Type Description paragraphIndexnumbersearchstringreplaceWithstringauthor?string
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 });
// 1. Parse DOCX and read as plain text
const reviewer = await DocxReviewer. fromBuffer ( await file. arrayBuffer (), 'AI Reviewer' );
const text = reviewer. getContentAsText ();
// 2. Send to LLM
const response = await openai.chat.completions. create ({
model: 'gpt-4o' ,
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: text }
],
});
const actions = JSON . parse (response.choices[ 0 ]?.message?.content || '{}' );
// 3. Apply review — author is set once on the reviewer
reviewer. applyReview ({
comments: actions.comments,
proposals: actions.replacements,
});
// 4. Return modified DOCX
const output = await reviewer. toBuffer ();
return new NextResponse (output, {
headers: {
'Content-Type' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
},
});
}
Review multiple DOCX files from the command line.
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' );
// Read content
const content = reviewer. getContentAsText ();
console. log (content);
// [0] (h1) Service Agreement
// [1] The liability cap is $50k per incident.
// [2] (table, row 1, col 1) Party A ...
// Add comments and changes directly
reviewer. addComment ( 1 , 'Liability cap is below industry standard.' );
reviewer. replace ( 1 , '$50k' , '$500k' );
// Export
const output = await reviewer. toBuffer ();
writeFileSync ( 'contract-reviewed.docx' , Buffer. from (output));
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-20250514' ,
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 });
Full working example: examples/agent-use-demo
Source: @eigenpal/docx-editor-agents