Skip to content

Feat/implement docx support task#330

Closed
vichazard wants to merge 5 commits into
wonderwhy-er:mainfrom
vichazard:feat/implement-docx-support-task
Closed

Feat/implement docx support task#330
vichazard wants to merge 5 commits into
wonderwhy-er:mainfrom
vichazard:feat/implement-docx-support-task

Conversation

@vichazard

@vichazard vichazard commented Feb 6, 2026

Copy link
Copy Markdown

User description

  1. add one tool write_docx for docx support task
  2. basic full CRUD operations for DOCX files (Read, Create, Update)
  • read: Convert existing DOCX files into markdown plus metadata, while preserving structure (headings, tables, images).
    for reading and parsing, I combined pizzip (DOCX as ZIP), @xmldom/xmldom (XML parsing of WordprocessingML), and docx (to represent content as Document, Paragraph, Table, ImageRun, etc.).

  • create: Generate new DOCX files from markdown content, including headings, tables, images, and basic text formatting.
    for creation, markdown is parsed line‑by‑line to detect tables, headings, images, and paragraphs, which are then mapped to the appropriate docx primitives and assembled into a new Document.

  • update: Apply high‑level operations to existing DOCX files (append markdown, insert tables, insert images, replace text) in a way that maintains the original layout.
    for updates, there are two paths: a fast XML path that edits word/document.xml directly for simple replaceText operations, and a structure path that uses the DocxStructure representation for structural changes like appending sections or inserting tables/images.
    all edits are expressed as high‑level operations validated via zod schemas: replaceText, appendMarkdown, insertTable, and insertImage.
    when all operations are replaceText, the system uses efficient XML replacement, which is safe for formatting and tables.
    when any structural operation is present, the system parses the existing DOCX to DocxStructure, applies each operation (e.g., convert the appended markdown into a temporary structure and merge it), and then rebuilds the DOCX from the updated element list.


CodeAnt-AI Description

Add full DOCX (Word) file support: read, create, and edit .docx files

What Changed

  • Read .docx files and return their content as Markdown with preserved headings, lists, tables, basic formatting, embedded images (base64), and extracted metadata (title, author, dates); reading returns a structured sections list and extracted images.
  • Create a new .docx from a Markdown string (headings, paragraphs, tables, images, bold/italic) and save it to disk.
  • Modify existing .docx files using high-level operations (replaceText, appendMarkdown, insertTable, insertImage); pure text replacements run directly in the DOCX XML to preserve formatting, structural edits preserve tables and images.
  • Expose a new write_docx tool/command (create or update modes) in the server API and filesystem tools, including a handler that returns clear success messages and operation summaries.
  • File handling and listing now recognize .docx files: reading returns combined text and image content items, getInfo returns DOCX-specific metadata, and multi-file summaries indicate DOCX files.

Impact

✅ Read DOCX as Markdown with images and metadata
✅ Create Word documents from Markdown
✅ Apply safe edits to DOCX (search/replace, append, insert tables/images)

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for reading Word documents (.docx) with automatic conversion to Markdown, including metadata extraction and embedded image handling.
    • Introduced DOCX file creation and editing capabilities, supporting operations such as text replacement, Markdown appending, table insertion, and image embedding.
    • New write_docx tool enables creating new Word documents or modifying existing ones with multiple editing operations.

@codeant-ai

codeant-ai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces comprehensive Microsoft Word document (.docx) support to the codebase by implementing a complete DOCX processing module with parsing, reading, writing, and editing capabilities. It integrates DOCX handling into the file handler architecture, adds a new write_docx server tool, and includes a test suite for DOCX reading functionality.

Changes

Cohort / File(s) Summary
Package Dependencies
package.json
Added runtime dependencies for DOCX processing: @xmldom/xmldom, docx, docxtemplater, jszip, and pizzip.
Core DOCX Type System & Error Handling
src/tools/docx/types.ts, src/tools/docx/errors.ts
Established comprehensive type definitions for DOCX elements, operations, metadata, and images; introduced DocxError class with error codes and withErrorContext helper for consistent error propagation.
DOCX Utilities & Infrastructure
src/tools/docx/utils.ts, src/tools/docx/index.ts
Provides data URL/file validation, image preparation, Markdown table parsing, file size formatting, and line ending normalization; main module index consolidates and re-exports all DOCX submodule exports.
DOCX Reading & Parsing
src/tools/docx/markdown.ts, src/tools/docx/parsers/*, src/tools/docx/structure.ts
Implements DOCX-to-Markdown conversion with image extraction and metadata handling; provides ZIP reading, XML parsing, table/paragraph/image extraction, and bidirectional structure preservation for DOCX documents.
DOCX Writing & Operations
src/tools/docx/builders/markdown-builder.ts, src/tools/docx/operations/*
Enables Markdown-to-DOCX creation with heading/table/image support; supports in-place text replacement via XML manipulation and high-level operations (replace text, append Markdown, insert tables/images) via structure editing.
Public Schemas & Filesystem Integration
src/tools/schemas.ts, src/tools/filesystem.ts
Added WriteDocxArgsSchema with validation for DOCX write operations and operation types; exposed writeDocx function for file creation/modification with operation support and telemetry.
File Handler Architecture
src/utils/files/base.ts, src/utils/files/docx.ts, src/utils/files/factory.ts, src/utils/files/index.ts
Extended FileMetadata and FileInfo types to include DOCX support; introduced DocxFileHandler implementing read/write/editRange/getInfo with metadata extraction, operation support, and error handling; integrated into file handler factory.
Server & Handler Integration
src/handlers/filesystem-handlers.ts, src/server.ts
Added handleWriteDocx command handler; registered write_docx tool with documentation for creation, modification, and operation modes; extended read_file tool documentation to include DOCX support with image embedding.
Test Suite
test/test-docx-reading.js
Introduced test scenarios for DOCX reading including metadata extraction, formatting preservation, error handling, and section parsing with formatted console output.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Server
    participant FileHandler as DocxFileHandler
    participant Parsers
    participant ZipUtils as ZipArchive
    participant Builders as Markdown Builder

    rect rgba(100, 150, 200, 0.5)
        Note over Client,Builders: DOCX Reading Flow
        Client->>Server: read_file with .docx path
        Server->>FileHandler: read(docxPath)
        FileHandler->>ZipUtils: createZipFromBuffer(buffer)
        FileHandler->>Parsers: parseDocxStructure(buffer)
        Parsers->>ZipUtils: readZipFileText(document.xml)
        Parsers->>ZipUtils: extractImagesFromZip()
        Parsers->>Parsers: parseParagraphElement()<br/>parseTableElement()
        Parsers-->>FileHandler: DocxStructure
        FileHandler->>Builders: parseDocxToMarkdown(source)
        Builders-->>FileHandler: markdown + metadata + images
        FileHandler-->>Server: FileResult with DOCX metadata
        Server-->>Client: markdown content + metadata
    end

    rect rgba(200, 150, 100, 0.5)
        Note over Client,Builders: DOCX Writing/Editing Flow
        Client->>Server: write_docx with operations
        Server->>FileHandler: write(docxPath, operations)
        alt Markdown Content
            FileHandler->>Builders: createDocxFromMarkdown(markdown)
            Builders-->>FileHandler: Buffer
        else DOCX Operations
            FileHandler->>FileHandler: editDocxWithOperations()
            FileHandler->>Parsers: parseDocxStructure(buffer)
            FileHandler->>FileHandler: applyOperationToStructure()
            FileHandler->>Builders: buildDocxFromStructure()
            Builders-->>FileHandler: Buffer
        end
        FileHandler-->>Server: void (write complete)
        Server-->>Client: success confirmation
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Allow reading urls and some cleanup #63 — Modifies filesystem read handlers and schemas for URL/file handling; directly related as DOCX reading is integrated into the same handleReadFile and readMultipleFiles code paths.
  • Feature/sheets handling #282 — Adds/extends file-handler architecture for Excel support; related architectural pattern where a new document format handler (DOCX vs. Excel) is integrated into the file-handler interface and factory.

Suggested labels

size:XXL

Suggested reviewers

  • serg33v
  • dmitry-ottic-ai

🐰 A rabbit hops through docs with glee,
New .docx files dance in harmony,
Parse and build with markdown light,
Word documents now shining bright! ✨📄

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main change: implementing DOCX support. However, it uses a generic feature format (Feat/) and includes task reference language, which is slightly verbose but still conveys the primary objective.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Feb 6, 2026
Comment thread src/server.ts
title: "Write/Modify DOCX",
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The write_docx tool is annotated with openWorldHint: false even though insertImage operations in the underlying implementation can reach out to HTTP/HTTPS URLs, so clients are misled into thinking the tool is strictly local, which is a security-relevant metadata bug; the hint should reflect that the tool may access the network. [security]

Severity Level: Major ⚠️
- ⚠️ ListTools metadata misleads clients about network access.
- ❌ write_docx image insertion may fetch remote images.
- ⚠️ Client UI policies may allow unsafe operations.
Suggested change
openWorldHint: false,
openWorldHint: true,
Steps of Reproduction ✅
1. Start the MCP server with the PR changes applied (see src/server.ts).

   - Inspect tool discovery by calling ListToolsRequestSchema handler which returns tool
   metadata including the write_docx tool. The write_docx tool's annotations block is
   present at src/server.ts:508-513 and shows openWorldHint: false.

2. Discover the tool in a client: call the MCP List Tools flow (ListToolsRequestSchema).

   - Client receives the write_docx tool metadata from src/server.ts and will interpret
   openWorldHint:false (no network access expected).

3. Invoke the tool via CallToolRequest with name "write_docx". The server routes this to
the handler at src/server.ts:1398 where the switch case calls
handlers.handleWriteDocx(args).

4. The handler path leads into the DOCX implementation that accepts insertImage operations
and intentionally does not validate HTTP(S) URLs. In src/tools/filesystem.ts (within the
writeDocx modification branch) the code only skips validation for paths starting with
'data:' or 'http' (so remote HTTP URLs are allowed to pass through), e.g. the insertImage
loop that checks:

   if (o.type === 'insertImage' && o.imagePath) {

       if (!o.imagePath.startsWith('data:') && !o.imagePath.startsWith('http')) {

           o.imagePath = await validatePath(o.imagePath);

       }

   }

   This means a client calling write_docx with an insertImage
   imagePath="http://example.com/img.png" will cause the system to accept the remote URL
   and the DOCX creation/update pipeline may fetch it.

Note: The existing annotations value openWorldHint:false appears to be an oversight
relative to the filesystem behavior above (the code intentionally treats http URLs as
external and doesn't validate them as local). The suggestion is not frivolous: it aligns
metadata with the actual runtime behavior.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/server.ts
**Line:** 512:512
**Comment:**
	*Security: The write_docx tool is annotated with openWorldHint: false even though insertImage operations in the underlying implementation can reach out to HTTP/HTTPS URLs, so clients are misled into thinking the tool is strictly local, which is a security-relevant metadata bug; the hint should reflect that the tool may access the network.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

children: headerCells.map((cell) => {
const textRuns = parseInlineFormatting(cell.trim());
textRuns.forEach(run => {
(run as any).bold = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Table header text is intended to be bolded, but the code assigns to a bold property on TextRun instances instead of using the docx API (e.g., the bold() method), so the formatting is likely ignored and headers won't actually appear bold in the generated DOCX. [logic error]

Severity Level: Major ⚠️
- ❌ Table headers not bold in generated DOCX.
- ⚠️ createDocxFromMarkdown used for DOCX generation (lines 28-145).
- ⚠️ Affects tables created via createTableFromMarkdown (lines 209-260).
Suggested change
(run as any).bold = true;
if (typeof (run as any).bold === 'function') {
(run as any).bold();
}
Steps of Reproduction ✅
1. Import and call createDocxFromMarkdown in src/tools/docx/builders/markdown-builder.ts
(function at lines 28-145) with markdown that contains a table (header row + separator).
The table detection lives at lines ~49-79 in the same file.

2. Execution reaches createTableFromMarkdown(lines: string[]) at
src/tools/docx/builders/markdown-builder.ts (function at lines 209-260). The header cell
mapping runs at lines 220-234.

3. While building the header row, the code at lines 221-224 sets (run as any).bold = true
on TextRun instances returned by parseInlineFormatting (lines 262-307).

4. The produced Document is packed via Packer.toBuffer (line 140). Inspecting the
generated .docx shows header text not visually bold because assigning a plain property to
existing TextRun instances does not update the docx run formatting (the code never
reconstructs TextRun with bold set).
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/builders/markdown-builder.ts
**Line:** 223:223
**Comment:**
	*Logic Error: Table header text is intended to be bolded, but the code assigns to a `bold` property on `TextRun` instances instead of using the docx API (e.g., the `bold()` method), so the formatting is likely ignored and headers won't actually appear bold in the generated DOCX.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread src/tools/docx/errors.ts
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = 'DocxError';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: When extending Error in TypeScript/JavaScript, failing to reset the prototype can cause instanceof checks against the custom error to fail in some runtimes/targets, so error instanceof DocxError in your wrapper may not work reliably and wrapped errors could be mishandled. [possible bug]

Severity Level: Major ⚠️
- ❌ Error identity lost in withErrorContext (src/tools/docx/errors.ts:60).
- ⚠️ Docx operation error handling may misclassify errors.
- ⚠️ createDocxError callers may receive wrapped errors.
Suggested change
this.name = 'DocxError';
Object.setPrototypeOf(this, new.target.prototype);
Steps of Reproduction ✅
1. Create an operation that throws a DocxError using the helper in this file: call
createDocxError(...) at src/tools/docx/errors.ts:83 (function createDocxError) to produce
a new DocxError instance.

2. Invoke the wrapper withErrorContext at src/tools/docx/errors.ts:60 by passing an
operation that immediately throws the DocxError from step 1. The try/catch in
withErrorContext is defined at src/tools/docx/errors.ts:65-78.

3. In the catch block (src/tools/docx/errors.ts:67-71) the code checks `if (error
instanceof DocxError)`; because the DocxError constructor does not call
Object.setPrototypeOf, some transpilation targets/runtimes may produce an instance for
which instanceof returns false.

4. As a result the wrapper proceeds to wrap the caught value into a new DocxError at
src/tools/docx/errors.ts:72-76 instead of re-throwing the original instance, losing
original identity/context and altering upstream error-handling behavior.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/errors.ts
**Line:** 16:16
**Comment:**
	*Possible Bug: When extending `Error` in TypeScript/JavaScript, failing to reset the prototype can cause `instanceof` checks against the custom error to fail in some runtimes/targets, so `error instanceof DocxError` in your wrapper may not work reliably and wrapped errors could be mishandled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const validatedOp = DocxReplaceTextOperationSchema.parse(op);
const tempPath = `${docxPath}.tmp${i}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Using a deterministic temp file name based only on the original path and loop index can cause race conditions and corrupted results when multiple concurrent edits target the same DOCX file, since different calls will overwrite each other's temp files. [race condition]

Severity Level: Major ⚠️
- ❌ Concurrent replaceText edits may corrupt DOCX output.
- ⚠️ replaceText fast-path used by batch text updates.
- ⚠️ Automated editors running parallel tasks affected.
Suggested change
const tempPath = `${docxPath}.tmp${i}`;
const tempPath = `${docxPath}.tmp-${process.pid}-${Date.now()}-${Math.random()}-${i}`;
Steps of Reproduction ✅
1. Invoke editDocxWithOperations at src/tools/docx/operations/index.ts:14 with a list of
replaceText operations so the "fast XML" branch is taken (operations.length > 0 and
allTextReplacements true).

2. The function enters the loop at src/tools/docx/operations/index.ts:37 and creates
deterministic temp files named `${docxPath}.tmp0`, `${docxPath}.tmp1`, ... at line 40.

3. Start two concurrent calls to editDocxWithOperations in parallel (for the same
docxPath) from application code (both calling src/tools/docx/operations/index.ts:14).
Because temp names are identical, one call's fs.writeFile at line 42 can overwrite the
other's temp file before replaceTextInDocxXml at line 43 reads it.

4. The overlapping writes produce incorrect intermediate ZIPs being passed to
replaceTextInDocxXml (src/tools/docx/operations/index.ts:43), resulting in corrupted or
partially-applied replacements and an incorrect final Buffer returned.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/operations/index.ts
**Line:** 40:40
**Comment:**
	*Race Condition: Using a deterministic temp file name based only on the original path and loop index can cause race conditions and corrupted results when multiple concurrent edits target the same DOCX file, since different calls will overwrite each other's temp files.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +208 to +210
const targetPath = rel.target.startsWith('word/')
? rel.target
: `word/${rel.target.replace(/^\/?/, '')}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: When resolving image relationships, absolute targets like "/word/media/image1.png" will end up with a duplicated "word/" prefix ("word/word/media/image1.png"), causing image loading to silently fail for such documents. [logic error]

Severity Level: Major ⚠️
- ❌ parseDocxToMarkdown image extraction fails.
- ⚠️ Images missing in markdown output.
- ⚠️ Downstream features lose embedded images.
Suggested change
const targetPath = rel.target.startsWith('word/')
? rel.target
: `word/${rel.target.replace(/^\/?/, '')}`;
const normalizedTarget = rel.target.replace(/^\/+/, '');
const targetPath = normalizedTarget.startsWith('word/')
? normalizedTarget
: `word/${normalizedTarget}`;
Steps of Reproduction ✅
1. Prepare a DOCX that references media with an absolute relationship Target (e.g.
"/word/media/image1.png"). See convert pipeline entry `parseDocxToMarkdown()` in
src/tools/docx/markdown.ts at 358-436 which calls buildImageResolver() (buildImageResolver
defined at 193-225).

2. Call parseDocxToMarkdown('/path/to/doc-with-absolute-rel.docx') (function at
src/tools/docx/markdown.ts:358). The function loads the DOCX and builds relMap via
extractRelationshipMap() at 176-191.

3. During markdown conversion, extractTextFromRun() (227-257) triggers resolveImage(relId)
which calls the resolver returned by buildImageResolver() (193-225). The resolver executes
the targetPath logic at lines 208-211.

4. If rel.target begins with a leading slash (e.g. "/word/media/image1.png"), current code
builds targetPath as `word/${rel.target.replace(/^\/?/, '')}` producing
"word/word/media/image1.png". readZipFileBuffer(zip, targetPath) (line 211) then fails to
find the file and image extraction silently returns ''.

Explanation: this is a concrete execution path: parseDocxToMarkdown -> buildImageResolver
-> resolver -> targetPath lines (208-211). The problem reproduces whenever a DOCX contains
relationship Targets that start with '/'.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/markdown.ts
**Line:** 208:210
**Comment:**
	*Logic Error: When resolving image relationships, absolute targets like "/word/media/image1.png" will end up with a duplicated "word/" prefix ("word/word/media/image1.png"), causing image loading to silently fail for such documents.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread src/tools/docx/types.ts
buffer: Buffer;
width?: number;
height?: number;
altText: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The prepared image representation requires altText while the insert-image operation makes altText optional, which forces callers to fabricate dummy values or use unsafe non-null assertions and can result in undefined being treated as a string downstream. Making altText optional in PreparedImage aligns the types and correctly models that alt text may be absent. [type error]

Severity Level: Major ⚠️
- ❌ DOCX insert-image operations may embed undefined alt text.
- ⚠️ Image-embedding XML serialization may omit alt attribute.
- ⚠️ Reader/writer image metadata inconsistent between steps.
Suggested change
altText: string;
altText?: string;
Steps of Reproduction ✅
1. Inspect type definitions in src/tools/docx/types.ts lines 122-128: PreparedImage
requires altText (line 122..128).

2. Inspect DocxInsertImageOperation in the same file at lines 152-158 where altText is
optional (line 152..158 shows altText?: string). This is a concrete mismatch inside the
same module.

3. Real-world trigger: build a docx image insertion flow that constructs a
DocxInsertImageOperation without altText (valid per the current type at lines 152-158),
and then pass that operation to the code path that converts image operations into
PreparedImage instances (the project's image-preparation / builder code reads the
operation and creates PreparedImage objects). At conversion time the code may assume
PreparedImage.altText is always a string (because the type requires it), so it either:

   - uses a non-null assertion (causing undefined to be passed forward), or

   - fabricates a dummy string, or

   - crashes/behaves incorrectly when downstream string APIs receive undefined.

   Note: the conversion function is outside this types file; the mismatch is provable by
   static analysis of lines 122-128 vs 152-158 in this file.

4. Observable failure modes: downstream image-embedding logic (image sizing/alt-text
serialization) will receive undefined for alt text, potentially causing incorrect XML
attributes or runtime errors in code that expects a string.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/types.ts
**Line:** 126:126
**Comment:**
	*Type Error: The prepared image representation requires `altText` while the insert-image operation makes `altText` optional, which forces callers to fabricate dummy values or use unsafe non-null assertions and can result in undefined being treated as a string downstream. Making `altText` optional in `PreparedImage` aligns the types and correctly models that alt text may be absent.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread src/tools/docx/types.ts
Comment on lines +146 to +150
export interface DocxInsertTableOperation {
type: 'insertTable';
markdownTable?: string;
rows?: string[][];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The insert-table operation type allows both markdownTable and rows to be omitted, so code that assumes at least one data source will exist can receive an operation with no table data and fail at runtime; defining DocxInsertTableOperation as a union that requires either markdownTable or rows prevents constructing such invalid operations. [logic error]

Severity Level: Major ⚠️
- ❌ Insert-table operations may cause runtime exceptions.
- ⚠️ DOCX update feature can produce empty/invalid tables.
- ⚠️ User-provided table inputs may be silently ignored.
Suggested change
export interface DocxInsertTableOperation {
type: 'insertTable';
markdownTable?: string;
rows?: string[][];
}
export type DocxInsertTableOperation =
| {
type: 'insertTable';
markdownTable: string;
rows?: string[][];
}
| {
type: 'insertTable';
rows: string[][];
markdownTable?: string;
};
Steps of Reproduction ✅
1. Examine table-op type in src/tools/docx/types.ts lines 146-150:
DocxInsertTableOperation currently allows both markdownTable and rows to be absent.

2. Create an operation object at runtime: const op = { type: 'insertTable' } — this is
permitted by the current interface (no required payload).

3. Pass op into the DOCX update pipeline that renders insertTable operations (the code
that applies DocxOperation lists to a document). That renderer will expect either
markdownTable or rows to exist and attempt to iterate or parse it.

4. Runtime result: renderer tries to read rows or parse markdownTable and either throws a
TypeError (e.g., cannot iterate undefined) or produces an empty/invalid table in the
output DOCX.

Note: types.ts shows the permissive shape (lines 146-150). The concrete renderer code is
outside this file, but this invalid operation shape is a realistic producer-consumer
mismatch when building operations from user input or client code.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/types.ts
**Line:** 146:150
**Comment:**
	*Logic Error: The insert-table operation type allows both `markdownTable` and `rows` to be omitted, so code that assumes at least one data source will exist can receive an operation with no table data and fail at runtime; defining `DocxInsertTableOperation` as a union that requires either `markdownTable` or `rows` prevents constructing such invalid operations.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread src/utils/files/docx.ts

return {
content,
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The read method returns markdown text as content but sets the MIME type to a DOCX binary MIME, which can cause downstream consumers to treat the response as a binary Word document instead of markdown text, leading to misrendering or incorrect handling of the data. [logic error]

Severity Level: Major ⚠️
- ❌ DocxFileHandler.read returns wrong MIME type.
- ⚠️ File preview/renderers mis-handle returned content.
- ⚠️ Saving response as .docx produces invalid files.
Suggested change
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
mimeType: 'text/markdown',
Steps of Reproduction ✅
1. Create a short script that imports the class declared at `src/utils/files/docx.ts:22`
(class DocxFileHandler) and calls its read method defined at `src/utils/files/docx.ts:36`:

   - Example code:

     const h = new DocxFileHandler();

     const res = await h.read('/tmp/sample.docx');

2. When read() executes it calls `parseDocxToMarkdown()` at `src/utils/files/docx.ts:39`,
which delegates to `src/tools/docx/markdown.ts:358` (function parseDocxToMarkdown) and
returns a markdown string in `res.content`.

3. Inspect the returned object: `res.content` contains markdown (see parseDocxToMarkdown
result) but `res.mimeType` is set to
`'application/vnd.openxmlformats-officedocument.wordprocessingml.document'` as returned at
`src/utils/files/docx.ts:71..74`.

4. Reproduce the consumer-visible mismatch concretely: write `res.content` to a file using
a small script that chooses extension based on `res.mimeType` (e.g., writes to
/tmp/output.docx because mimeType indicates DOCX). Attempt to open `/tmp/output.docx` in
Word — the file will contain markdown text, not a valid DOCX archive, demonstrating the
incorrect MIME labeling and resulting misbehavior.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/utils/files/docx.ts
**Line:** 73:73
**Comment:**
	*Logic Error: The read method returns markdown text as content but sets the MIME type to a DOCX binary MIME, which can cause downstream consumers to treat the response as a binary Word document instead of markdown text, leading to misrendering or incorrect handling of the data.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread src/tools/docx/utils.ts
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: SVG image files are accepted as valid in the validator but are not mapped to the correct MIME type in the image preparation logic, so they are incorrectly labeled as PNG when embedded, which can break downstream consumers that rely on the MIME type. [logic error]

Severity Level: Major ⚠️
- ⚠️ Prepared image MIME may mismatch content.
- ⚠️ Downstream consumers relying on mimeType affected.
- ⚠️ validateImageFile permits SVGs but mapping inconsistent.
Suggested change
};
'.svg': 'image/svg+xml',
Steps of Reproduction ✅
1. Open src/tools/docx/utils.ts and locate validateImageFile at lines 96-104: it lists
validExtensions and includes '.svg' (line 98).

2. Locate prepareImageForDocx at lines 269-346; after reading a local image it uses the
MIME mapping at lines 321-331.

3. Provide a local SVG path (e.g., './assets/vector.svg') to prepareImageForDocx from a
test or REPL. validateImageFile will accept '.svg' (line ~98) and fs.readFile will
succeed.

4. After reading, execution reaches the mimeTypes mapping (lines 321-331) which lacks
'.svg', so mimeType becomes the default 'image/png', producing an incorrect MIME type for
the SVG file.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/docx/utils.ts
**Line:** 330:330
**Comment:**
	*Logic Error: SVG image files are accepted as valid in the validator but are not mapped to the correct MIME type in the image preparation logic, so they are incorrectly labeled as PNG when embedded, which can break downstream consumers that rely on the MIME type.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment thread test/test-docx-reading.js
Comment on lines +220 to +223
// Summary
const passed = results.filter(r => r === true).length;
const total = results.length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Skipped tests (when the sample DOCX file is missing and the test functions return without a boolean) are still pushed into the results array and counted in total, so the summary and exit code will treat skipped tests as failed, causing the script to exit with status 1 even when all executed tests passed. The fix is to exclude undefined (skipped) entries from the results used for the summary. [logic error]

Severity Level: Major ⚠️
- ❌ Test runner reports false failures and exits non-zero.
- ⚠️ Local developer test runs show misleading summary.
- ⚠️ CI jobs running this script may fail unexpectedly.
Suggested change
// Summary
const passed = results.filter(r => r === true).length;
const total = results.length;
// Summary (ignore skipped tests that returned undefined)
const effectiveResults = results.filter(r => r !== undefined);
const passed = effectiveResults.filter(r => r === true).length;
const total = effectiveResults.length;
Steps of Reproduction ✅
1. Run the test script: execute `node test/test-docx-reading.js`. The file calls
`runAllTests()` (see `test/test-docx-reading.js:236`), which in turn executes the test
functions.

2. `runAllTests()` pushes each test result into `results` at
`test/test-docx-reading.js:215-219` (lines with `results.push(await
testBasicDocxReading());` ... `results.push(await testMetadataExtraction());`).

3. If the sample DOCX file is missing, `testBasicDocxReading()` returns early without a
boolean (`return;`) inside its inner catch block at `test/test-docx-reading.js:41-46` (the
`await fs.access(samplePath)` catch). The function therefore yields `undefined`.

4. That `undefined` is appended to `results`. The summary code at
`test/test-docx-reading.js:222-223` computes `passed = results.filter(r => r ===
true).length` and `total = results.length`, so skipped (`undefined`) entries increase
`total` and cause `passed !== total`.

5. The script then calls `process.exit(passed === total ? 0 : 1)` at
`test/test-docx-reading.js:234`, causing the process to exit with status 1 even if all
executed (non-skipped) tests returned true. Observed effect: a run with skipped tests
reports failures and returns non-zero exit code.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** test/test-docx-reading.js
**Line:** 220:223
**Comment:**
	*Logic Error: Skipped tests (when the sample DOCX file is missing and the test functions return without a boolean) are still pushed into the `results` array and counted in `total`, so the summary and exit code will treat skipped tests as failed, causing the script to exit with status 1 even when all executed tests passed. The fix is to exclude `undefined` (skipped) entries from the results used for the summary.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai

codeant-ai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tools/filesystem.ts (1)

780-857: ⚠️ Potential issue | 🟡 Minor

DOCX metadata is not surfaced in getFileInfo.

The function handles metadata for PDF (line 843), Excel (line 832), image (line 838), and binary (line 851) file types, but there's no corresponding block for DOCX. This means DOCX-specific metadata (author, title, sections, images, etc.) won't appear in getFileInfo results even though it's defined in FileMetadata.

Proposed addition after the PDF block (after line 848)
         // For PDF files
         if (fileInfo.metadata.isPdf) {
             info.isPdf = true;
             info.totalPages = fileInfo.metadata.totalPages;
             if (fileInfo.metadata.title) info.title = fileInfo.metadata.title;
             if (fileInfo.metadata.author) info.author = fileInfo.metadata.author;
         }
+
+        // For DOCX files
+        if (fileInfo.metadata.isDocx) {
+            info.isDocx = true;
+            if (fileInfo.metadata.title) info.title = fileInfo.metadata.title;
+            if (fileInfo.metadata.author) info.author = fileInfo.metadata.author;
+            if (fileInfo.metadata.subject) info.subject = fileInfo.metadata.subject;
+            if (fileInfo.metadata.sectionCount !== undefined) info.sectionCount = fileInfo.metadata.sectionCount;
+            if (fileInfo.metadata.imageCount !== undefined) info.imageCount = fileInfo.metadata.imageCount;
+        }
🤖 Fix all issues with AI agents
In `@src/tools/docx/builders/markdown-builder.ts`:
- Around line 220-224: The header mapping mutates constructed TextRun objects
((run as any).bold = true) which does nothing; instead update the code to create
bold runs at construction time—either call the TextRun fluent API (e.g., use the
run.bold() chain) when building runs inside the header mapping or refactor
parseInlineFormatting to accept an optional bold flag and have it construct and
return runs with bold set. Update the headerCells.map usage to pass that flag
into parseInlineFormatting or to apply the fluent .bold() on each returned run
immediately when creating the runs so header text is actually bold in the
generated docx.

In `@src/tools/docx/markdown.ts`:
- Around line 88-191: Remove the duplicated helper implementations and import
the canonical implementations from the existing modules: replace local isUrl
with the export from src/tools/docx/utils.ts; replace readZipFileText and
readZipFileBuffer with the exports from parsers/zip-reader.ts; replace
getMimeTypeForTarget with the export from parsers/image-extractor.ts; replace
getElementChildren, getAttributeValue, getHeadingLevelFromParagraph and
extractRelationshipMap with the exports from parsers/xml-parser.ts — update the
top-level imports and delete the local function definitions (isUrl,
readZipFileText, readZipFileBuffer, getMimeTypeForTarget, getElementChildren,
getAttributeValue, getHeadingLevelFromParagraph, extractRelationshipMap) so this
module reuses the single source of truth.
- Around line 94-102: The loadDocxToBuffer function should validate HTTP
responses when source is a URL: after fetching in loadDocxToBuffer (and using
isUrl to detect URLs), check response.ok and if false throw or return an error
that includes response.status and response.statusText (and optionally the URL)
before calling response.arrayBuffer(), so non-2xx responses (404/500) are
surfaced instead of silently returning an error page body.
- Around line 279-302: convertTableToMarkdown currently uses recursive
getElementsByTagName('w:tr'|'w:tc'|'w:p') which pulls in nested table
rows/cells/paras; replace those calls with a non‑recursive child iteration (e.g.
use a getElementChildren helper or loop over element.childNodes and filter by
node.nodeName === 'w:tr' / 'w:tc' / 'w:p') so only direct children are
processed; update the loops in convertTableToMarkdown that build rowNodes, cells
and paragraphs to use this child-filtering approach, keeping use of
extractParagraphText and escapeTableCell unchanged.

In `@src/tools/docx/operations/index.ts`:
- Around line 34-52: The loop writes temporary files and leaks them on errors
and is wasteful; update the logic so replaceTextInDocxXml is called with an
in-memory Buffer instead of writing temp files (either add a new API like
replaceTextInDocxXmlBuffer(buffer, search, replace) or overload
replaceTextInDocxXml to accept a Buffer), then call that with modifiedBuffer in
the for-loop (using DocxReplaceTextOperationSchema.parse(op) to get validatedOp)
and remove tempPath writes/unlinks; if you can't change replaceTextInDocxXml
immediately, wrap the per-iteration work (writeFile, call replaceTextInDocxXml,
unlink) in a try/finally to ensure tempPath is always unlinked even when
replaceTextInDocxXml throws, referencing modifiedBuffer, tempPath,
replaceTextInDocxXml and DocxReplaceTextOperationSchema to locate the code to
change.

In `@src/tools/docx/operations/operation-handlers.ts`:
- Around line 66-74: Merging appendStructure.relationships directly can
overwrite existing keys; instead generate new unique relIds for each entry from
appendStructure.relationships, insert them into structure.relationships and
structure.images using the new keys, and update any references in appended
elements to point to the new relIds before pushing them into structure.elements.
Concretely, inside the merge logic that iterates appendStructure.relationships
and appendStructure.elements, create a function to produce a newRelId (e.g.,
based on existing keys in structure.relationships), map oldRelId -> newRelId,
set structure.relationships.set(newRelId, rel) and for any matching entry in
appendStructure.images move imgBuffer to structure.images.set(newRelId,
imgBuffer), then walk each element in appendStructure.elements and replace
references to the oldRelId (attributes like rId, r:embed, relationshipId, href
targets or similar) with newRelId before pushing elements into
structure.elements so no collisions occur.

In `@src/tools/docx/operations/xml-replacer.ts`:
- Around line 44-48: The current naive replacement uses documentXml.replace(...)
to produce modifiedXml and will miss targets split across multiple <w:r>/<w:t>
runs (variables involved: documentXml, modifiedXml, escapedSearch,
escapedReplace); change the implementation to either (a) pre-process the XML to
merge adjacent run text nodes into contiguous searchable text per paragraph/run
group and perform replacements on that merged text, then re-split or reassign
the replaced text back into the original <w:t> elements, or (b) implement a
run-aware search that walks <w:t> elements, concatenates their text to match
escapedSearch across run boundaries, performs the substitution, and writes the
result back across the same runs; alternatively, if you opt not to implement
merging, add a clear comment and documentation near xml-replacer.ts explaining
this limitation and that documentXml.replace cannot match text spanning multiple
runs (mention escapedSearch/escapedReplace so reviewers can find the relevant
variables).
- Around line 34-48: The code uses escapedSearch in new RegExp(...) but
escapeXml doesn't escape regex metacharacters, causing incorrect matches and
ReDoS risk; fix by treating the search as a literal: either escape regex
metacharacters after escapeXml (add an escapeRegex helper and call it on
escapedSearch before building the RegExp) or avoid RegExp entirely and use a
literal replace (String.prototype.replaceAll or split/join) when creating
modifiedXml from documentXml; update references to
escapedSearch/escapedReplace/modifiedXml accordingly.

In `@src/tools/docx/parsers/paragraph-parser.ts`:
- Around line 83-94: The code forces every image to 600×400 and swallows errors;
update the w:drawing / w:pict branch (where resolveImageRelId(child), images,
and ImageRun are used) to first try to read the drawing extent (search the child
for the a:ext cx/cy attributes) and convert those EMU values to pixels (use a
named constant like EMU_PER_PIXEL) to compute width/height; if extent is
missing, fall back to a configurable default or the image's intrinsic
dimensions. Replace the empty catch with proper logging (e.g., logger.warn or
console.warn) that includes the relId and error details so failures are visible,
and only skip the image after logging.

In `@src/tools/docx/parsers/table-parser.ts`:
- Around line 25-33: The parser is using getElementsByTagName which recurses
into nested tables and pulls rows/cells/paragraphs from inner tables; update the
logic in the table parsing flow to iterate only direct children instead of using
getElementsByTagName: in the block that builds rows from table (where rowNodes =
table.getElementsByTagName('w:tr')), replace that recursive lookup with a
direct-child iteration (e.g., an element children filter) so only immediate
'w:tr' children are processed; apply the same change inside parseTableRow
(replace getElementsByTagName('w:tc') with direct-child iteration for immediate
'w:tc' cells) and inside parseTableCell (replace getElementsByTagName('w:p')
with direct-child iteration for immediate 'w:p' paragraphs) so nested w:tbl
content is not mixed into parent table parsing.

In `@src/tools/docx/types.ts`:
- Around line 47-92: The four interfaces DocxMetadata, DocxImage, DocxSection,
and DocxParseResult are duplicated; remove the duplicate declarations from
markdown.ts and have markdown.ts import those types from types.ts instead,
updating any local references to use the imported types; ensure index.ts
continues to re-export the canonical types (from types.ts) so public API remains
the same and adjust any internal imports that were pointing to ./types.js to
reference the single types.ts definitions.

In `@src/tools/docx/utils.ts`:
- Around line 250-258: PreparedImage is duplicated in utils.ts and types.ts;
remove the duplicate declaration from utils.ts and instead import {
PreparedImage } from the existing src/tools/docx/types.ts where needed (e.g., in
functions like any helpers in utils that currently reference PreparedImage).
Update utils.ts exports so it no longer re-exports PreparedImage (ensure
index.ts and operations.ts still re-export from types.ts), and run a quick
compile to fix any missing import sites and adjust import paths to reference the
single source of truth PreparedImage in types.ts.

In `@src/tools/filesystem.ts`:
- Around line 961-974: The loop handling insertImage operations silently
swallows validatePath errors and keeps unvalidated imagePath; instead let
validation failures surface so the operation fails: remove or rethrow inside the
try/catch around validatePath for the insertImage branch (referencing
validatePath, o.imagePath, and the insertImage operation) so errors propagate
like writePdf's sourcePdfPath validation, and tighten the URL check from
startsWith('http') to explicit startsWith('http://') || startsWith('https://')
to avoid false positives.

In `@test/test-docx-reading.js`:
- Around line 39-47: When the sample DOCX is missing, each test
(testBasicDocxReading, testFormattingPreservation, testMetadataExtraction)
should explicitly return a distinct sentinel (e.g., the string 'skipped' or a
SKIPPED Symbol) instead of undefined; update the early-exit blocks that
currently log "Sample DOCX file not found" to return that sentinel. Then update
runAllTests to treat results strictly (count passes where r === true, failures
where r === false, and skipped where r === 'skipped' or the SKIPPED Symbol) and
report/exit accordingly so skipped tests are not treated as failures. Ensure all
three test functions and runAllTests reference the same sentinel constant
(SKIPPED) to keep behavior consistent.
🟡 Minor comments (10)
src/tools/filesystem.ts-986-993 (1)

986-993: ⚠️ Potential issue | 🟡 Minor

Remove unused outputPath option or document its purpose.

editDocxWithOperations accepts outputPath in its options but never uses it—only baseDir is referenced. The function returns a Buffer, and the caller correctly writes it to the target path. However, the unused outputPath option in DocxEditOptions creates a misleading API contract.

Either remove the option or clarify its intended purpose if it's meant for future use.

test/test-docx-reading.js-125-138 (1)

125-138: ⚠️ Potential issue | 🟡 Minor

Tests 2 and 4 have no assertions — they always pass.

testFormattingPreservation (and similarly testMetadataExtraction) only logs whether formatting markers are detected but never asserts or returns false on unexpected results. These are diagnostic scripts, not tests. If the sample DOCX contains bold text and parsing breaks, this test would still report .

Consider adding actual assertions, e.g.:

         const hasBold = result.markdown.includes('**');
+        if (!hasBold && !hasItalic && !hasHeadings && !hasLists) {
+            log(colors.red, '✗ No formatting detected — expected at least one marker');
+            return false;
+        }
src/handlers/filesystem-handlers.ts-213-214 (1)

213-214: ⚠️ Potential issue | 🟡 Minor

DOCX files in readMultipleFiles lack image embedding, creating inconsistency with single-file reads.

The summary (line 213-214) correctly labels DOCX files, but the content loop (lines 229-259) has no DOCX-specific handling. DOCX results fall through to the generic else text branch (line 252), dropping embedded images. Meanwhile, handleReadFile extracts and includes these images (lines 130-138).

The issue stems from readMultipleFiles only creating a payload for PDF files (line 564); DOCX files get payload: undefined despite images being available in metadata. To maintain parity, add a DOCX branch to extract images:

Suggested fix
            } else if (result.isImage && result.mimeType) {
                // For image files, add an image content item
                contentItems.push({
                    type: "image",
                    data: result.content,
                    mimeType: result.mimeType
                });
+           } else if (result.isDocx && result.payload?.images?.length) {
+               for (const image of result.payload.images) {
+                   contentItems.push({ type: "image", data: image.data, mimeType: image.mimeType });
+               }
+               contentItems.push({ type: "text", text: `\n--- ${result.path} contents: ---\n${result.content}` });
            } else {
                // For text files, add a text summary

(Note: This assumes payload.images is populated for DOCX, which may require updating line 564 to include DOCX image data in the payload.)

Also applies to: 229-259

src/tools/docx/parsers/paragraph-parser.ts-79-82 (1)

79-82: ⚠️ Potential issue | 🟡 Minor

Remove redundant text: '\n' when using break property.

new TextRun({ text: '\n', break: 1 }) includes unnecessary text: '\n'. In docx v8.5.0, the break property itself inserts the line break element; the text: '\n' won't render as a line break in the document. Use new TextRun({ break: 1 }) instead.

src/tools/docx/markdown.ts-457-503 (1)

457-503: ⚠️ Potential issue | 🟡 Minor

Double ZIP parsing and ReDoS-susceptible regex in metadata extraction.

Two issues:

  1. Unnecessary second ZIP library: The buffer is already loaded with PizZip at Line 390, but extractMetadata loads it again with JSZip (Line 460). Reuse the existing PizZip zip instance instead.

  2. Static analysis flagged ReDoS on the dynamically constructed regexes (Lines 468, 474, 480). While the tag values are hardcoded strings from Lines 492–499 and the risk is low, the DOMParser (already imported at Line 13) should be used for parsing core properties XML instead of fragile regex.

src/server.ts-447-514 (1)

447-514: ⚠️ Potential issue | 🟡 Minor

write_docx default-overwrites the original, but write_pdf forbids it — inconsistent UX.

The write_docx description (Lines 460-463) encourages overwriting the original file by default when outputPath is omitted, while write_pdf (Lines 393, 403) explicitly states "NEVER overwrite the original file" and requires a new filename in outputPath.

This inconsistency could confuse LLM clients that learn patterns from one tool and apply them to the other. Consider aligning the default behavior, or at minimum making the distinction very prominent in the description.

src/tools/docx/markdown.ts-524-526 (1)

524-526: ⚠️ Potential issue | 🟡 Minor

No-op regex: list formatting replacement is identity.

markdown = markdown.replace(/\n([*-]\s)/g, '\n$1');

This replaces \n followed by * or - with… the exact same thing. If the intent was to ensure a blank line before lists, the replacement should be '\n\n$1' or similar.

src/tools/docx/utils.ts-96-104 (1)

96-104: ⚠️ Potential issue | 🟡 Minor

SVG accepted by validateImageFile but missing from MIME type map in prepareImageForDocx.

validateImageFile (line 98) includes .svg in its validExtensions, so SVG files pass validation. However, the MIME type map inside prepareImageForDocx (lines 323-330) doesn't include .svg, causing it to default to image/png — which is incorrect for SVG files and may produce corrupt image output.

Additionally, note that the docx library's ImageRun may not support SVG at all. Either add .svg to the MIME map or remove it from the valid extensions to keep them consistent.

🐛 Option A: Add SVG to the MIME map
         '.bmp': 'image/bmp',
         '.webp': 'image/webp',
+        '.svg': 'image/svg+xml',
       };
🐛 Option B: Remove SVG from valid extensions if unsupported by docx library
-  const validExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'];
+  const validExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'];

Also applies to: 321-331

src/tools/docx/builders/markdown-builder.ts-262-307 (1)

262-307: ⚠️ Potential issue | 🟡 Minor

Inline formatting parser has unclosed-marker fallback gap.

When ** is found at line 269 but no closing ** exists, the code falls through without consuming the ** characters — currentText doesn't accumulate them, and i isn't advanced past them. This causes the first * to be re-evaluated at line 284 as a potential italic marker, producing garbled output for malformed input like **unclosed.

🛡️ Suggested fix: fall through to accumulate the literal characters
     if (text.substring(i, i + 2) === '**') {
       if (currentText) {
         runs.push(new TextRun({ text: currentText }));
         currentText = '';
       }
       const closeIndex = text.indexOf('**', i + 2);
       if (closeIndex !== -1) {
         const boldText = text.substring(i + 2, closeIndex);
         runs.push(new TextRun({ text: boldText, bold: true }));
         i = closeIndex + 2;
         continue;
+      } else {
+        // No closing **, treat as literal text
+        currentText += '**';
+        i += 2;
+        continue;
       }
     }
     
     // Check for *italic*
     if (text[i] === '*' && text[i + 1] !== '*') {
       if (currentText) {
         runs.push(new TextRun({ text: currentText }));
         currentText = '';
       }
       const closeIndex = text.indexOf('*', i + 1);
       if (closeIndex !== -1) {
         const italicText = text.substring(i + 1, closeIndex);
         runs.push(new TextRun({ text: italicText, italics: true }));
         i = closeIndex + 1;
         continue;
+      } else {
+        // No closing *, treat as literal text
+        currentText += '*';
+        i += 1;
+        continue;
       }
     }
src/utils/files/docx.ts-243-250 (1)

243-250: ⚠️ Potential issue | 🟡 Minor

Import DocxErrorCode and use DocxErrorCode.GET_INFO_FAILED instead of the string literal.

Line 247 uses the string literal 'GET_INFO_FAILED' instead of DocxErrorCode.GET_INFO_FAILED. This is inconsistent with the rest of the codebase, where all other files (src/tools/docx/structure.ts, utils.ts, operations/*) properly import and use the enum. Add the import and use the enum constant to maintain type safety and consistency.

🧹 Nitpick comments (17)
src/utils/files/base.ts (1)

133-154: Consider the memory impact of embedding full image data in metadata.

The images field stores base64 image data (data: string) directly in the metadata object. For DOCX files with many or large embedded images, this could significantly increase memory usage when the metadata is serialized or passed around. Consider whether a summary (e.g., just id, mimeType, originalSize) would suffice for metadata, with full image data accessible on-demand via a separate API.

Also, author and title (already present from PDF metadata at lines 128-129) appear to be reused for DOCX without an explicit comment — this is fine if intentional, but worth a note for clarity.

src/tools/docx/operations/operation-handlers.ts (1)

16-22: Unused createRequire — the require binding on line 18 is never used.

The docx library is imported via ESM on line 20. The CJS require from createRequire is not referenced afterward. Remove the dead code.

Proposed fix
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
-// `@ts-ignore`
-import * as docx from 'docx';
+// `@ts-ignore`
+import * as docx from 'docx';
src/tools/docx/parsers/zip-reader.ts (1)

16-18: Consider wrapping new PizZip(buffer) with error context.

If the buffer isn't a valid ZIP (e.g., corrupted DOCX), PizZip will throw a raw error without DOCX-specific context. Since the DocxError/withErrorContext pattern is established in this codebase, wrapping here would improve error diagnostics.

Proposed improvement
+import { DocxError, DocxErrorCode } from '../errors.js';
+
 export function createZipFromBuffer(buffer: Buffer): ZipArchive {
-  return new PizZip(buffer);
+  try {
+    return new PizZip(buffer);
+  } catch (error) {
+    throw new DocxError(
+      `Failed to parse DOCX as ZIP archive: ${error instanceof Error ? error.message : String(error)}`,
+      DocxErrorCode.INVALID_DOCX,
+      { originalError: error instanceof Error ? error.stack : undefined }
+    );
+  }
 }
src/tools/docx/errors.ts (1)

9-18: Consider narrowing code from string to DocxErrorCode for type safety.

The constructor accepts code: string, but the codebase defines DocxErrorCode enum for all known codes. Narrowing the type would prevent passing arbitrary strings and ensure all errors use standardized codes.

Proposed change
 export class DocxError extends Error {
   constructor(
     message: string,
-    public readonly code: string,
+    public readonly code: DocxErrorCode,
     public readonly context?: Record<string, unknown>
   ) {

This would also require updating withErrorContext (line 62) to accept only DocxErrorCode instead of DocxErrorCode | string.

test/test-docx-reading.js (1)

6-6: Tests import from compiled dist/ output.

Importing from ../dist/tools/docx/markdown.js couples tests to a prior build step. If the build is stale, tests will pass against old code. Consider using a test framework that supports TypeScript directly (e.g., vitest, tsx) or document the build prerequisite.

src/tools/docx/parsers/paragraph-parser.ts (1)

1-7: createRequire is imported but unused.

Line 1 imports createRequire and line 3 creates a require function, but neither is actually used — the docx library is imported via the ES import statement on line 5. This is dead code.

Proposed fix
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
 // `@ts-ignore`
 import * as docx from 'docx';
src/tools/docx/operations/index.ts (1)

3-3: Unused import: z from zod.

z is imported but never referenced in this file.

♻️ Remove unused import
-import { z } from 'zod';
src/tools/docx/markdown.ts (1)

391-395: Docxtemplater is instantiated only for validation, then discarded.

The instance is never used for actual parsing — all work is done via raw XML. This adds unnecessary startup cost and a dependency. If the intent is just to validate the DOCX structure, consider removing this or making it opt-in.

src/tools/docx/parsers/table-parser.ts (1)

6-8: Unused createRequire import.

createRequire is imported and require is assigned (Lines 6–8), but neither is used — docx is loaded via ES import at Line 10.

♻️ Remove unused import
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
src/tools/docx/builders/markdown-builder.ts (1)

1-5: Unused createRequire / require — dead code.

createRequire is imported and called on lines 1 and 3, but the resulting require function is never used anywhere in this file. This looks like a leftover from an earlier approach.

🧹 Remove unused import and variable
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
 // `@ts-ignore`
 import * as docx from 'docx';
src/tools/docx/structure.ts (1)

9-15: Unused createRequire / require — same dead code as in markdown-builder.ts.

require is created but never called. Only Document and Packer are actually used, and they come from the ESM import * as docx.

🧹 Remove unused import and variable
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
-// `@ts-ignore`
-import * as docx from 'docx';
+// `@ts-ignore` - docx library has incomplete type definitions
+import * as docx from 'docx';
 
 const { Document, Packer } = docx as any;
src/utils/files/docx.ts (3)

28-31: Parameter path shadows the imported path module.

In canHandle (and similarly in read, write, editRange, getInfo), the parameter is named path, which shadows the Node.js path module imported on line 7. This is a maintenance hazard — if any method later needs path.extname() or similar, the shadowed import will cause subtle bugs.

Also, the variable ext on line 29 is actually the full lowercased path, not just the extension.

♻️ Rename parameter to `filePath` across all methods
-    canHandle(path: string): boolean {
-        const ext = path.toLowerCase();
+    canHandle(filePath: string): boolean {
+        const ext = filePath.toLowerCase();
         return this.extensions.some(e => ext.endsWith(e));
     }

Apply the same rename (pathfilePath) to read, write, editRange, and getInfo.


111-140: No runtime validation of DocxOperation[] in write and editRange.

When content is an array (line 133), it's cast to DocxOperation[] without any runtime check that the elements have valid type fields. Passing malformed objects (e.g., [{ foo: "bar" }]) would propagate to editDocxWithOperations and produce confusing errors.

A lightweight guard (e.g., checking that each element has a type property matching known operation types) would improve error messages for callers.


89-99: read silently swallows errors and returns them as content.

On parse failure, the handler returns a FileResult with the error message as content and mimeType: 'text/plain'. The caller has no easy programmatic way to distinguish success from failure without inspecting metadata.error. Consider whether this should throw or at least document this contract clearly, since other FileHandler implementations may throw.

src/tools/docx/index.ts (1)

55-79: Consider narrowing the public API surface for utilities.

This barrel exports ~16 utility functions (e.g., escapeRegExp, fileExists, formatFileSize, parseDataUrl, splitMarkdownLines) that are general-purpose helpers unlikely to be needed by external consumers. Exposing them increases the API surface to maintain and couples consumers to internal implementation details.

Consider keeping only DOCX-specific utilities (like prepareImageForDocx, createImageRun, isDocxPath) in the public API, and importing the rest directly from ./utils.js where needed internally.

src/tools/docx/utils.ts (1)

334-341: Hardcoded default image dimensions (600×400) with no override path.

prepareImageForDocx always returns width: 600, height: 400 regardless of the actual image dimensions. While the comment says "can be customized," the function signature provides no way to pass desired dimensions. This means all embedded images will be rendered at the same fixed size.

Consider accepting optional width/height parameters (they already exist on PreparedImage), or detecting actual image dimensions from the buffer.

src/tools/docx/types.ts (1)

6-14: All runtime type aliases are any — reduces type safety across the DOCX module.

While this is pragmatic given incomplete docx library typings, it means no compile-time checks for misuse of DocxParagraph, DocxTable, etc. throughout the codebase.

Consider gradually replacing these with more specific types as the module stabilizes (e.g., import type { Paragraph } from 'docx' if available, or at least structural types capturing the properties you depend on).

Comment on lines +220 to +224
children: headerCells.map((cell) => {
const textRuns = parseInlineFormatting(cell.trim());
textRuns.forEach(run => {
(run as any).bold = true;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

docx npm library TextRun bold property mutation after construction

💡 Result:

In docx (dolanmiu/docx) you generally can’t reliably “flip” bold by mutating some options object after a TextRun is constructed:

  • The run styling options type (IRunStylePropertiesOptions) marks properties like bold as Readonly, i.e. they’re intended to be set at creation time (or via provided builder methods), not mutated later. [1]
  • The supported way to apply bold after construction is the fluent/builder API: new TextRun("Name:").bold() (this mutates the TextRun instance via its method, not by mutating your original options). [2]

What to do instead

  • Set it at construction:
    const r = new TextRun({ text: "Hello", bold: true });
  • Or use the builder method:
    const r = new TextRun("Hello").bold();
  • If you need to change bold conditionally after the fact (especially to turn it off), the simplest approach is usually to create a new TextRun with the desired bold value, because the API surface is primarily “set properties when building the run”, not “mutable style setters for every property”.

Sources: [1] [2]


🏁 Script executed:

# Find the markdown-builder.ts file and examine lines around 220-224
find . -name "markdown-builder.ts" -type f

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 121


🏁 Script executed:

# Once found, read the file to understand the context
head -250 src/tools/docx/builders/markdown-builder.ts | tail -50

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1459


🏁 Script executed:

# Search for parseInlineFormatting function definition
rg "function parseInlineFormatting|parseInlineFormatting\s*=" -A 20

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1654


🏁 Script executed:

# Check if there are any uses of .bold() method in the codebase for TextRun
rg "\.bold\(" src/ --type ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 58


🌐 Web query:

docx TextRun.bold() builder method fluent API

💡 Result:

In the dolanmiu docx (JS/TS) library, TextRun supports a fluent/builder method .bold() (no arguments) that sets the run’s bold formatting to true:

import { TextRun } from "docx";

const run = new TextRun("Name:").bold();

This is shown in the project’s “Styling with JS” fluent API docs, where .bold() is listed as a run-formatting method. [1]

If you’re using the object/options form instead of fluent chaining, bold is also available as a boolean run property (bold?: boolean):

const run = new TextRun({ text: "Name:", bold: true });

The bold option is part of IRunStylePropertiesOptions. [2]

Sources: [1] [2]


Bug: Mutating bold on constructed TextRun has no effect.

In the docx library, TextRun properties are Readonly and the internal XML representation is built at construction time. Assigning (run as any).bold = true after the fact does not update the output. Header cells will not render as bold.

Use the supported fluent API instead:

       children: headerCells.map((cell) => {
         const textRuns = parseInlineFormatting(cell.trim());
-        textRuns.forEach(run => {
-          (run as any).bold = true;
-        });
+        textRuns.forEach(run => run.bold());
         return new TableCell({

Alternatively, refactor parseInlineFormatting to accept an optional bold flag and construct runs with that property set at initialization.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
children: headerCells.map((cell) => {
const textRuns = parseInlineFormatting(cell.trim());
textRuns.forEach(run => {
(run as any).bold = true;
});
children: headerCells.map((cell) => {
const textRuns = parseInlineFormatting(cell.trim());
textRuns.forEach(run => run.bold());
return new TableCell({
🤖 Prompt for AI Agents
In `@src/tools/docx/builders/markdown-builder.ts` around lines 220 - 224, The
header mapping mutates constructed TextRun objects ((run as any).bold = true)
which does nothing; instead update the code to create bold runs at construction
time—either call the TextRun fluent API (e.g., use the run.bold() chain) when
building runs inside the header mapping or refactor parseInlineFormatting to
accept an optional bold flag and have it construct and return runs with bold
set. Update the headerCells.map usage to pass that flag into
parseInlineFormatting or to apply the fluent .bold() on each returned run
immediately when creating the runs so header text is actually bold in the
generated docx.

Comment on lines +88 to +191
const isUrl = (source: string): boolean =>
source.startsWith('http://') || source.startsWith('https://');

/**
* Load DOCX file as buffer
*/
async function loadDocxToBuffer(source: string): Promise<Buffer> {
if (isUrl(source)) {
const response = await fetch(source);
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} else {
return await fs.readFile(source);
}
}

function readZipFileText(zip: any, filePath: string): string | null {
const file = zip.file(filePath);
if (!file) return null;
if (typeof file.asText === 'function') {
return file.asText();
}
if (typeof file.asBinary === 'function') {
return Buffer.from(file.asBinary(), 'binary').toString('utf8');
}
return null;
}

function readZipFileBuffer(zip: any, filePath: string): Buffer | null {
const file = zip.file(filePath);
if (!file) return null;
if (typeof file.asUint8Array === 'function') {
return Buffer.from(file.asUint8Array());
}
if (typeof file.asNodeBuffer === 'function') {
return file.asNodeBuffer();
}
if (typeof file.asBinary === 'function') {
return Buffer.from(file.asBinary(), 'binary');
}
return null;
}

function getMimeTypeForTarget(target: string): string {
const ext = path.extname(target).toLowerCase();
const mimeTypes: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
};
return mimeTypes[ext] || 'application/octet-stream';
}

function escapeTableCell(text: string): string {
return text.replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
}

function getElementChildren(node: Node): Element[] {
const children: Element[] = [];
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
children.push(child as Element);
}
}
return children;
}

function getAttributeValue(node: Element, name: string): string | null {
return node.getAttribute(name) || node.getAttribute(`w:${name}`) || null;
}

function getHeadingLevelFromParagraph(paragraph: Element): number | null {
const pPr = paragraph.getElementsByTagName('w:pPr')[0];
if (!pPr) return null;
const pStyle = pPr.getElementsByTagName('w:pStyle')[0];
if (!pStyle) return null;
const styleVal = getAttributeValue(pStyle, 'val');
if (!styleVal) return null;
const match = styleVal.match(/heading\s*([1-6])/i);
if (!match) return null;
return Number(match[1]);
}

function extractRelationshipMap(relsXml: string | null): Map<string, { target: string; type: string }> {
const relMap = new Map<string, { target: string; type: string }>();
if (!relsXml) return relMap;
const relDoc = new DOMParser().parseFromString(relsXml, 'application/xml');
const rels = relDoc.getElementsByTagName('Relationship');
for (let i = 0; i < rels.length; i++) {
const rel = rels[i];
const id = rel.getAttribute('Id');
const type = rel.getAttribute('Type') || '';
const target = rel.getAttribute('Target') || '';
if (id && target) {
relMap.set(id, { target, type });
}
}
return relMap;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Significant code duplication with the parsers/ modules.

The following functions are copy-pasted from already-exported modules in this PR:

Function Duplicate of
isUrl (L88) src/tools/docx/utils.ts
readZipFileText (L104) src/tools/docx/parsers/zip-reader.ts
readZipFileBuffer (L116) src/tools/docx/parsers/zip-reader.ts
getMimeTypeForTarget (L131) src/tools/docx/parsers/image-extractor.ts
getElementChildren (L149) src/tools/docx/parsers/xml-parser.ts
getAttributeValue (L160) src/tools/docx/parsers/xml-parser.ts
getHeadingLevelFromParagraph (L164) src/tools/docx/parsers/xml-parser.ts
extractRelationshipMap (L176) src/tools/docx/parsers/xml-parser.ts

Import from the canonical sources instead to avoid drift and reduce the module's size by ~100 lines.

🤖 Prompt for AI Agents
In `@src/tools/docx/markdown.ts` around lines 88 - 191, Remove the duplicated
helper implementations and import the canonical implementations from the
existing modules: replace local isUrl with the export from
src/tools/docx/utils.ts; replace readZipFileText and readZipFileBuffer with the
exports from parsers/zip-reader.ts; replace getMimeTypeForTarget with the export
from parsers/image-extractor.ts; replace getElementChildren, getAttributeValue,
getHeadingLevelFromParagraph and extractRelationshipMap with the exports from
parsers/xml-parser.ts — update the top-level imports and delete the local
function definitions (isUrl, readZipFileText, readZipFileBuffer,
getMimeTypeForTarget, getElementChildren, getAttributeValue,
getHeadingLevelFromParagraph, extractRelationshipMap) so this module reuses the
single source of truth.

Comment on lines +94 to +102
async function loadDocxToBuffer(source: string): Promise<Buffer> {
if (isUrl(source)) {
const response = await fetch(source);
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} else {
return await fs.readFile(source);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

loadDocxToBuffer from URL: no response status check.

If fetch returns a non-2xx response (404, 500, etc.), response.arrayBuffer() will silently succeed with the error page body. Check response.ok before consuming the body.

🛡️ Proposed fix
 async function loadDocxToBuffer(source: string): Promise<Buffer> {
     if (isUrl(source)) {
         const response = await fetch(source);
+        if (!response.ok) {
+            throw new Error(`Failed to fetch DOCX from URL: ${response.status} ${response.statusText}`);
+        }
         const arrayBuffer = await response.arrayBuffer();
         return Buffer.from(arrayBuffer);
     } else {
🤖 Prompt for AI Agents
In `@src/tools/docx/markdown.ts` around lines 94 - 102, The loadDocxToBuffer
function should validate HTTP responses when source is a URL: after fetching in
loadDocxToBuffer (and using isUrl to detect URLs), check response.ok and if
false throw or return an error that includes response.status and
response.statusText (and optionally the URL) before calling
response.arrayBuffer(), so non-2xx responses (404/500) are surfaced instead of
silently returning an error page body.

Comment on lines +279 to +302
function convertTableToMarkdown(table: Element, resolveImage: (relId: string | null) => string): string | null {
const rows: string[][] = [];
const rowNodes = table.getElementsByTagName('w:tr');
for (let i = 0; i < rowNodes.length; i++) {
const row = rowNodes[i];
const cells = row.getElementsByTagName('w:tc');
const rowCells: string[] = [];
for (let j = 0; j < cells.length; j++) {
const cell = cells[j];
const paragraphs = cell.getElementsByTagName('w:p');
const cellTexts: string[] = [];
for (let k = 0; k < paragraphs.length; k++) {
const text = extractParagraphText(paragraphs[k], resolveImage).trim();
if (text) {
cellTexts.push(text);
}
}
const combined = cellTexts.length > 0 ? cellTexts.join('<br>') : ' ';
rowCells.push(escapeTableCell(combined));
}
if (rowCells.length > 0) {
rows.push(rowCells);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

getElementsByTagName('w:tr') is recursive and will pick up nested table rows.

If a DOCX table contains nested tables, getElementsByTagName traverses into nested w:tbl elements and returns all w:tr descendants, mixing inner and outer rows. The same applies to Line 284 for w:tc and Line 288 for w:p.

Use direct children iteration (like getElementChildren) and filter by nodeName instead:

♻️ Sketch of non-recursive row iteration
-  const rowNodes = table.getElementsByTagName('w:tr');
-  for (let i = 0; i < rowNodes.length; i++) {
-    const row = rowNodes[i];
+  const rowNodes = getElementChildren(table).filter(c => c.nodeName === 'w:tr');
+  for (const row of rowNodes) {

Apply the same pattern for w:tc within a row and w:p within a cell.

🤖 Prompt for AI Agents
In `@src/tools/docx/markdown.ts` around lines 279 - 302, convertTableToMarkdown
currently uses recursive getElementsByTagName('w:tr'|'w:tc'|'w:p') which pulls
in nested table rows/cells/paras; replace those calls with a non‑recursive child
iteration (e.g. use a getElementChildren helper or loop over element.childNodes
and filter by node.nodeName === 'w:tr' / 'w:tc' / 'w:p') so only direct children
are processed; update the loops in convertTableToMarkdown that build rowNodes,
cells and paragraphs to use this child-filtering approach, keeping use of
extractParagraphText and escapeTableCell unchanged.

Comment on lines +34 to +52
if (allTextReplacements && operations.length > 0) {
let modifiedBuffer: Buffer = await fs.readFile(docxPath);

for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const validatedOp = DocxReplaceTextOperationSchema.parse(op);
const tempPath = `${docxPath}.tmp${i}`;

await fs.writeFile(tempPath, modifiedBuffer);
modifiedBuffer = await replaceTextInDocxXml(
tempPath,
validatedOp.search,
validatedOp.replace
);

await fs.unlink(tempPath).catch(() => {});
}

return modifiedBuffer;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Temp files leak on error and are unnecessary given the in-memory buffer.

Two concerns with the temp-file loop:

  1. Leak on error: If replaceTextInDocxXml throws at iteration i, the temp file written at Line 42 is never cleaned up because unlink at Line 49 is skipped.
  2. Unnecessary disk I/O: You already hold modifiedBuffer in memory (Line 35). Writing it to disk only to have replaceTextInDocxXml read it back is wasteful. Consider refactoring replaceTextInDocxXml (or adding a variant) to accept a Buffer directly.

At minimum, wrap the loop body in try/finally to ensure cleanup:

🛡️ Proposed fix for temp file cleanup
       for (let i = 0; i < operations.length; i++) {
         const op = operations[i];
         const validatedOp = DocxReplaceTextOperationSchema.parse(op);
         const tempPath = `${docxPath}.tmp${i}`;
-        
-        await fs.writeFile(tempPath, modifiedBuffer);
-        modifiedBuffer = await replaceTextInDocxXml(
-          tempPath,
-          validatedOp.search,
-          validatedOp.replace
-        );
-        
-        await fs.unlink(tempPath).catch(() => {});
+
+        try {
+          await fs.writeFile(tempPath, modifiedBuffer);
+          modifiedBuffer = await replaceTextInDocxXml(
+            tempPath,
+            validatedOp.search,
+            validatedOp.replace
+          );
+        } finally {
+          await fs.unlink(tempPath).catch(() => {});
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (allTextReplacements && operations.length > 0) {
let modifiedBuffer: Buffer = await fs.readFile(docxPath);
for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const validatedOp = DocxReplaceTextOperationSchema.parse(op);
const tempPath = `${docxPath}.tmp${i}`;
await fs.writeFile(tempPath, modifiedBuffer);
modifiedBuffer = await replaceTextInDocxXml(
tempPath,
validatedOp.search,
validatedOp.replace
);
await fs.unlink(tempPath).catch(() => {});
}
return modifiedBuffer;
if (allTextReplacements && operations.length > 0) {
let modifiedBuffer: Buffer = await fs.readFile(docxPath);
for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const validatedOp = DocxReplaceTextOperationSchema.parse(op);
const tempPath = `${docxPath}.tmp${i}`;
try {
await fs.writeFile(tempPath, modifiedBuffer);
modifiedBuffer = await replaceTextInDocxXml(
tempPath,
validatedOp.search,
validatedOp.replace
);
} finally {
await fs.unlink(tempPath).catch(() => {});
}
}
return modifiedBuffer;
🤖 Prompt for AI Agents
In `@src/tools/docx/operations/index.ts` around lines 34 - 52, The loop writes
temporary files and leaks them on errors and is wasteful; update the logic so
replaceTextInDocxXml is called with an in-memory Buffer instead of writing temp
files (either add a new API like replaceTextInDocxXmlBuffer(buffer, search,
replace) or overload replaceTextInDocxXml to accept a Buffer), then call that
with modifiedBuffer in the for-loop (using
DocxReplaceTextOperationSchema.parse(op) to get validatedOp) and remove tempPath
writes/unlinks; if you can't change replaceTextInDocxXml immediately, wrap the
per-iteration work (writeFile, call replaceTextInDocxXml, unlink) in a
try/finally to ensure tempPath is always unlinked even when replaceTextInDocxXml
throws, referencing modifiedBuffer, tempPath, replaceTextInDocxXml and
DocxReplaceTextOperationSchema to locate the code to change.

Comment on lines +25 to +33
const rowNodes = table.getElementsByTagName('w:tr');

for (let i = 0; i < rowNodes.length; i++) {
const rowNode = rowNodes[i];
const row = parseTableRow(rowNode, images);
if (row) {
rows.push(row);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same nested getElementsByTagName issue — rows from nested tables will be included.

getElementsByTagName('w:tr') at Line 25 recurses into nested w:tbl elements. The same applies to w:tc (Line 53) and w:p (Line 80). Use direct-child iteration to avoid mixing rows/cells from nested tables.

♻️ Proposed fix using getElementChildren
+import { getElementChildren } from './xml-parser.js';
+
 export function parseTableElement(
   table: Element,
   images: Map<string, Buffer>
 ): DocxTable | null {
   const rows: InstanceType<typeof TableRow>[] = [];
-  const rowNodes = table.getElementsByTagName('w:tr');
-
-  for (let i = 0; i < rowNodes.length; i++) {
-    const rowNode = rowNodes[i];
+  const rowNodes = getElementChildren(table).filter(c => c.nodeName === 'w:tr');
+
+  for (const rowNode of rowNodes) {
     const row = parseTableRow(rowNode, images);

Apply the same to parseTableRow (for w:tc) and parseTableCell (for w:p).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rowNodes = table.getElementsByTagName('w:tr');
for (let i = 0; i < rowNodes.length; i++) {
const rowNode = rowNodes[i];
const row = parseTableRow(rowNode, images);
if (row) {
rows.push(row);
}
}
import { getElementChildren } from './xml-parser.js';
export function parseTableElement(
table: Element,
images: Map<string, Buffer>
): DocxTable | null {
const rows: InstanceType<typeof TableRow>[] = [];
const rowNodes = getElementChildren(table).filter(c => c.nodeName === 'w:tr');
for (const rowNode of rowNodes) {
const row = parseTableRow(rowNode, images);
if (row) {
rows.push(row);
}
}
🤖 Prompt for AI Agents
In `@src/tools/docx/parsers/table-parser.ts` around lines 25 - 33, The parser is
using getElementsByTagName which recurses into nested tables and pulls
rows/cells/paragraphs from inner tables; update the logic in the table parsing
flow to iterate only direct children instead of using getElementsByTagName: in
the block that builds rows from table (where rowNodes =
table.getElementsByTagName('w:tr')), replace that recursive lookup with a
direct-child iteration (e.g., an element children filter) so only immediate
'w:tr' children are processed; apply the same change inside parseTableRow
(replace getElementsByTagName('w:tc') with direct-child iteration for immediate
'w:tc' cells) and inside parseTableCell (replace getElementsByTagName('w:p')
with direct-child iteration for immediate 'w:p' paragraphs) so nested w:tbl
content is not mixed into parent table parsing.

Comment thread src/tools/docx/types.ts
Comment on lines +47 to +92
/**
* DOCX metadata structure
*/
export interface DocxMetadata {
title?: string;
author?: string;
creator?: string;
subject?: string;
description?: string;
creationDate?: Date;
modificationDate?: Date;
lastModifiedBy?: string;
revision?: string;
fileSize?: number;
}

/**
* Embedded image information
*/
export interface DocxImage {
id: string;
data: string; // Base64-encoded
mimeType: string;
altText?: string;
originalSize?: number;
}

/**
* DOCX section/paragraph structure
*/
export interface DocxSection {
type: 'heading' | 'paragraph' | 'list' | 'table' | 'image';
content: string;
level?: number;
images?: DocxImage[];
}

/**
* Complete DOCX parse result
*/
export interface DocxParseResult {
markdown: string;
metadata: DocxMetadata;
images: DocxImage[];
sections?: DocxSection[];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

DocxMetadata, DocxImage, DocxSection, and DocxParseResult are duplicated in markdown.ts.

These four interfaces are defined identically in both src/tools/docx/types.ts (here) and src/tools/docx/markdown.ts (lines 17-82). The barrel index.ts re-exports them from markdown.ts, making these copies in types.ts unused via the public API — but other internal files importing from ./types.js would get a different (identical but distinct) type.

Consolidate into a single canonical location. Since types.ts is the intended central type file, consider having markdown.ts import from types.ts instead of re-declaring them.

🤖 Prompt for AI Agents
In `@src/tools/docx/types.ts` around lines 47 - 92, The four interfaces
DocxMetadata, DocxImage, DocxSection, and DocxParseResult are duplicated; remove
the duplicate declarations from markdown.ts and have markdown.ts import those
types from types.ts instead, updating any local references to use the imported
types; ensure index.ts continues to re-export the canonical types (from
types.ts) so public API remains the same and adjust any internal imports that
were pointing to ./types.js to reference the single types.ts definitions.

Comment thread src/tools/docx/utils.ts
Comment on lines +250 to +258
* Image data prepared for DOCX embedding
*/
export interface PreparedImage {
buffer: Buffer;
width?: number;
height?: number;
altText: string;
mimeType: string;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

PreparedImage interface is duplicated — defined both here and in types.ts.

This interface is defined identically at src/tools/docx/types.ts:122-128 and here at lines 252-258. Both are exported, and the barrel index.ts re-exports from both utils.ts (line 78) and types.ts (indirectly via operations.ts). This DRY violation will cause drift as one copy gets updated but not the other.

♻️ Remove the duplicate and import from `types.ts`
-/**
- * Image data prepared for DOCX embedding
- */
-export interface PreparedImage {
-  buffer: Buffer;
-  width?: number;
-  height?: number;
-  altText: string;
-  mimeType: string;
-}
+// Re-export PreparedImage from the canonical location
+export type { PreparedImage } from './types.js';
🤖 Prompt for AI Agents
In `@src/tools/docx/utils.ts` around lines 250 - 258, PreparedImage is duplicated
in utils.ts and types.ts; remove the duplicate declaration from utils.ts and
instead import { PreparedImage } from the existing src/tools/docx/types.ts where
needed (e.g., in functions like any helpers in utils that currently reference
PreparedImage). Update utils.ts exports so it no longer re-exports PreparedImage
(ensure index.ts and operations.ts still re-export from types.ts), and run a
quick compile to fix any missing import sites and adjust import paths to
reference the single source of truth PreparedImage in types.ts.

Comment thread src/tools/filesystem.ts
Comment on lines +961 to +974
for (const o of content) {
if (o.type === 'insertImage' && o.imagePath) {
// Only validate local paths (not data URLs or web URLs)
if (!o.imagePath.startsWith('data:') && !o.imagePath.startsWith('http')) {
try {
o.imagePath = await validatePath(o.imagePath);
} catch {
// If validation fails, keep original path
// Error will be caught during operation execution
}
}
}
operations.push(o);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent error swallowing on path validation weakens security guarantees.

When validatePath fails for an image path (lines 965-970), the error is silently caught and the original unvalidated path is retained. This is inconsistent with how writePdf handles sourcePdfPath validation (it does not catch errors), and it could mask real path-traversal violations. If the path fails validation, the operation should fail explicitly rather than proceeding with an unvalidated path.

Proposed fix
             if (o.type === 'insertImage' && o.imagePath) {
                 // Only validate local paths (not data URLs or web URLs)
-                if (!o.imagePath.startsWith('data:') && !o.imagePath.startsWith('http')) {
-                    try {
-                        o.imagePath = await validatePath(o.imagePath);
-                    } catch {
-                        // If validation fails, keep original path
-                        // Error will be caught during operation execution
-                    }
+                if (!o.imagePath.startsWith('data:') && !o.imagePath.startsWith('http://') && !o.imagePath.startsWith('https://')) {
+                    o.imagePath = await validatePath(o.imagePath);
                 }
             }

Note: The startsWith('http') check is also too loose — it matches any string starting with "http" (e.g., httpfoo://...). The fix above tightens this to http:// and https://.

🤖 Prompt for AI Agents
In `@src/tools/filesystem.ts` around lines 961 - 974, The loop handling
insertImage operations silently swallows validatePath errors and keeps
unvalidated imagePath; instead let validation failures surface so the operation
fails: remove or rethrow inside the try/catch around validatePath for the
insertImage branch (referencing validatePath, o.imagePath, and the insertImage
operation) so errors propagate like writePdf's sourcePdfPath validation, and
tighten the URL check from startsWith('http') to explicit startsWith('http://')
|| startsWith('https://') to avoid false positives.

Comment thread test/test-docx-reading.js
Comment on lines +39 to +47
try {
// Check if sample file exists
try {
await fs.access(samplePath);
} catch {
log(colors.yellow, 'Sample DOCX file not found, skipping test');
log(colors.cyan, `Expected location: ${samplePath}`);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Skipped tests are silently counted as failures.

When sample.docx is absent, testBasicDocxReading, testFormattingPreservation, and testMetadataExtraction return undefined (no explicit return value). In runAllTests, the check results.filter(r => r === true) counts undefined as a failure. This means the suite always exits non-zero in any environment without the sample file (e.g. CI), making it impossible to distinguish "skipped" from "genuinely failed."

Consider returning a distinct sentinel or tracking skipped tests separately:

Proposed fix sketch for runAllTests
-    const passed = results.filter(r => r === true).length;
-    const total = results.length;
+    const passed = results.filter(r => r === true).length;
+    const skipped = results.filter(r => r === undefined || r === null).length;
+    const failed = results.filter(r => r === false).length;
+    const total = results.length;
     
-    if (passed === total) {
+    if (failed === 0 && passed > 0) {
         log(colors.green, `\n✓ All tests passed (${passed}/${total})`);
+    } else if (failed === 0) {
+        log(colors.yellow, `\n⚠ All tests skipped (missing sample.docx)`);
     } else {
-        log(colors.yellow, `\n⚠ ${passed}/${total} tests passed`);
+        log(colors.yellow, `\n⚠ ${passed} passed, ${failed} failed, ${skipped} skipped`);
     }
     
-    process.exit(passed === total ? 0 : 1);
+    process.exit(failed > 0 ? 1 : 0);

Also applies to: 112-117, 172-177

🤖 Prompt for AI Agents
In `@test/test-docx-reading.js` around lines 39 - 47, When the sample DOCX is
missing, each test (testBasicDocxReading, testFormattingPreservation,
testMetadataExtraction) should explicitly return a distinct sentinel (e.g., the
string 'skipped' or a SKIPPED Symbol) instead of undefined; update the
early-exit blocks that currently log "Sample DOCX file not found" to return that
sentinel. Then update runAllTests to treat results strictly (count passes where
r === true, failures where r === false, and skipped where r === 'skipped' or the
SKIPPED Symbol) and report/exit accordingly so skipped tests are not treated as
failures. Ensure all three test functions and runAllTests reference the same
sentinel constant (SKIPPED) to keep behavior consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants