Skip to content

Feat/implement docx support#335

Open
vichazard wants to merge 10 commits intowonderwhy-er:mainfrom
vichazard:feat/Implement-docx-support
Open

Feat/implement docx support#335
vichazard wants to merge 10 commits intowonderwhy-er:mainfrom
vichazard:feat/Implement-docx-support

Conversation

@vichazard
Copy link

@vichazard vichazard commented Feb 10, 2026

User description

Summary by CodeRabbit

  • New Features

    • Full DOCX support: read outlines, extract metadata and text, create DOCX from structured content, and a two-step read→write DOCX editing workflow.
    • Rich patch-based edits: replace/insert/delete paragraphs, set styles/colors, edit tables, insert images, update headers and hyperlinks.
    • Multi-file workflows now detect and summarize DOCX files alongside PDFs.
  • Quality

    • Structural validation for DOCX writes and clearer error/warning reporting; improved multi-file PDF/content summarization.

CodeAnt-AI Description

Add first-class DOCX read/write tools and local Desktop Commander routing

What Changed

  • New read_docx tool returns a compact JSON outline (paragraphs with bodyChildIndex, stylesSeen, table and image summaries) for token-efficient, single-call document inspection.
  • New write_docx tool supports two workflows: create a new professional DOCX (complete styles, defaults, ZIP structure) and update an existing DOCX with patch-style operations (replace/insert/delete paragraphs, set paragraph styles/colors, edit table cells, insert tables and images, replace hyperlinks, and header replacements). Updates preserve styles, images, numbering and other DOCX structure and validate body/table counts before writing.
  • Server and filesystem handlers now recognize .docx files: read_file/read_multiple return DOCX metadata, paragraph counts, body XML and guidance to use read_docx/write_docx for edits; new handlers expose read_docx/write_docx endpoints; Desktop Commander guidance forces local DC tools to be preferred for DOCX/PDF/local file operations.

Impact

✅ Precise DOCX editing workflows (single read_docx → one write_docx call)
✅ Preserve formatting when creating or modifying DOCX
✅ Avoid accidental cloud-based edits of local DOCX/PDF files

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

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 10, 2026

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
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds full DOCX support across the stack: dependencies, DOM & ZIP utilities, readers/writers/creators, many patch ops and builders, schema and server tool integration, filesystem/file-handler wiring, and handler-level call routing for read_docx/write_docx.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added @xmldom/xmldom and pizzip.
Server / tooling / handlers
src/server.ts, src/tools/schemas.ts, src/handlers/filesystem-handlers.ts
Added read_docx/write_docx tools, integrated ReadDocxArgsSchema/WriteDocxArgsSchema, embedded DOCX guidance/priorities, and wired new handler cases and handler implementations.
DOCX core (DOM / ZIP / constants / validate)
src/tools/docx/dom.ts, src/tools/docx/zip.ts, src/tools/docx/constants.ts, src/tools/docx/validate.ts
XML parse/serialize and DOM helpers, DOCX ZIP load/save helpers, constants (namespaces, MIME, EMU), and snapshot/invariant validation.
Read / metadata / legacy modify
src/tools/docx/read.ts, src/tools/docx/modify.ts
DOCX outline/paragraph/table/image extraction, metadata/word counts, body XML extraction, and legacy modify/replace/write helpers.
Write orchestration & ops dispatcher
src/tools/docx/write.ts, src/tools/docx/ops/index.ts
Orchestrator writeDocxPatched applying DocxOp sequences with snapshots, validation, and save; applyOp dispatcher now accepts optional zip.
Operation handlers
src/tools/docx/ops/*
Many new op implementations (replace/insert/delete/style/table/hyperlink/header/image/…); each adds specific DOM/ZIP actions and returns OpResult.
Builders & element helpers
src/tools/docx/builders/*, src/tools/docx/builders/utils.ts
Paragraph, table, image builders, XML escaping utilities, and image embedding with relationships/content-type handling.
Relationships & content-types
src/tools/docx/relationships.ts
Add image relationship creation and ensure [Content_Types].xml entries.
Create-from-content
src/tools/docx/create.ts
createDocxNew builds a full DOCX ZIP from structured content using builders.
Types & barrel exports
src/tools/docx/types.ts, src/tools/docx/index.ts
Comprehensive DOCX types, op unions, content structure types, and barrel exports for read/write/ops.
Filesystem & file handlers
src/tools/filesystem.ts, src/utils/files/docx.ts, src/utils/files/factory.ts, src/utils/files/base.ts
Added DocxPayload and multi-file payload union, writeDocx API, DocxFileHandler (read/editRange/getInfo), factory integration and file-type priority updates.
Builders index & exports
src/tools/docx/builders/index.ts
Centralized exports for builders and utilities.
Minor updates / summaries
src/handlers/filesystem-handlers.ts, other handlers
Multi-file read summaries recognize DOCX payloads; enhanced PDF page/image traversal; new DOCX content blocks and error paths.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Handler as handleReadDocx
    participant Zip as loadDocxZip/getDocumentXml
    participant Dom as parseXml
    participant Reader as readDocxOutline

    Client->>Server: call read_docx { path }
    Server->>Handler: invoke handleReadDocx(args)
    Handler->>Zip: loadDocxZip(path)
    Zip-->>Handler: PizZip
    Handler->>Zip: getDocumentXml(zip)
    Zip-->>Handler: document.xml
    Handler->>Dom: parseXml(document.xml)
    Dom-->>Handler: Document DOM
    Handler->>Reader: readDocxOutline(path)
    Reader-->>Handler: ReadDocxResult
    Handler-->>Server: JSON response
    Server-->>Client: outline + metadata
Loading
sequenceDiagram
    participant Client
    participant Server
    participant Handler as handleWriteDocx
    participant Writer as writeDocxPatched
    participant Zip as loadDocxZip/getDocumentXml/saveDocxZip
    participant Dom as parseXml/serializeXml
    participant OpDisp as applyOp
    participant Validator as validateInvariants

    Client->>Server: call write_docx { inputPath?, outputPath, ops/content }
    Server->>Handler: invoke handleWriteDocx(args)
    Handler->>Writer: writeDocxPatched(inputPath, outputPath, ops)
    Writer->>Zip: loadDocxZip(inputPath)
    Zip-->>Writer: PizZip
    Writer->>Zip: getDocumentXml(zip)
    Zip-->>Writer: document.xml
    Writer->>Dom: parseXml(document.xml)
    Dom-->>Writer: Document DOM
    Writer->>Validator: captureSnapshot(body) (before)
    loop for each op
      Writer->>OpDisp: applyOp(body, op, zip?)
      OpDisp-->>Writer: OpResult
    end
    Writer->>Validator: captureSnapshot(body) (after)
    Validator-->>Writer: validateInvariants(before, after)
    Writer->>Dom: serializeXml(doc)
    Dom-->>Writer: modified XML
    Writer->>Zip: saveDocxZip(zip, newXml, outputPath)
    Zip-->>Writer: saved
    Writer-->>Handler: WriteDocxResult
    Handler-->>Server: JSON response
    Server-->>Client: result + stats
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • serg33v
  • dmitry-ottic-ai

Poem

🐇
I nibbled XML, stitched images in a zip,
hopped through paragraphs with a careful tip.
I painted runs, inserted rows, and saved the lot —
a floppy-eared docx, tidy in my hop-and-plot!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (12 files):

⚔️ README.md (content)
⚔️ package-lock.json (content)
⚔️ package.json (content)
⚔️ server.json (content)
⚔️ src/handlers/filesystem-handlers.ts (content)
⚔️ src/server.ts (content)
⚔️ src/tools/filesystem.ts (content)
⚔️ src/tools/schemas.ts (content)
⚔️ src/utils/capture.ts (content)
⚔️ src/utils/files/base.ts (content)
⚔️ src/utils/files/factory.ts (content)
⚔️ src/version.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/implement docx support' directly describes the main change: adding comprehensive DOCX read/write support as a major feature.

✏️ 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
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/Implement-docx-support
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/tools/docx/index.ts (1)

1-39: LGTM — clean barrel exports.

Minor nit: there are two // Types comment blocks (Lines 10 and 24) that could be consolidated or differentiated (e.g., "Content types" vs "Result/Op types").

src/server.ts (1)

25-34: DC_PRIORITY_NOTE is only injected into the get_config tool description.

The routing guidance is only visible when the LLM reads the get_config tool listing. If the intent is for this guidance to influence all tool selection, consider whether embedding it in only one tool's description is sufficient. Some LLM clients may not surface all tool descriptions to the model simultaneously.

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

197-227: after/before mutual exclusivity not enforced at the type level.

Both InsertTableOp and InsertImageOp document that after and before are mutually exclusive, but the current interfaces allow both or neither to be set. This aligns with reviewer feedback #13 — consider encoding the constraint in the type system so invalid combinations are caught at compile time, in addition to any Zod runtime checks.

♻️ Example: encode mutual exclusivity via union
-export interface InsertTableOp {
-    type: 'insert_table';
-    /** Exact trimmed text of the paragraph to insert AFTER. Mutually exclusive with `before`. */
-    after?: string;
-    /** Exact trimmed text of the paragraph to insert BEFORE. Mutually exclusive with `after`. */
-    before?: string;
-    headers?: string[];
-    rows: string[][];
-    colWidths?: number[];
-    style?: string;
-}
+interface InsertTableBase {
+    type: 'insert_table';
+    headers?: string[];
+    rows: string[][];
+    colWidths?: number[];
+    style?: string;
+}
+
+export type InsertTableOp =
+    | (InsertTableBase & { after: string; before?: never })
+    | (InsertTableBase & { before: string; after?: never });

Apply the same pattern to InsertImageOp.


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 10, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 10, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Overwrite safety
    The write_docx tool text states "Never overwrites the input — always writes to outputPath". Enforce this in the server/handler layer: require outputPath and validate it's different from inputPath, or explicitly reject operations that would overwrite the source.

  • Unvalidated docx patch ops & path checks
    handleWriteDocx calls writeDocxPatched(parsed.inputPath, parsed.outputPath, parsed.ops as any) without validating parsed.ops shape or ensuring inputPath/outputPath are allowed. This can lead to runtime errors or unauthorized file writes. Validate ops (e.g., array of patch ops), ensure paths are allowed, and sanitize inputs.

  • DOCX content size / type assumptions
    The new DOCX handler returns concatenated body XML and extractedText directly as a single large text block. This assumes fileResult.content and meta.extractedText are always strings and may return very large payloads to the caller. Consider explicit size limits, type checks, and safer presentation (e.g., return structured JSON or trimmed previews) to avoid memory/response bloat and accidental data exposure.

  • PDF iteration guards
    The additions iterate PDF pages and images assuming result.payload.pages and page.images always exist and are arrays. If pages or images are undefined or not arrays this will throw at runtime. Add guards and robust typing before looping; also avoid unbounded concatenation of binary image data into the content array.

  • Save and atomicity
    writeDocxPatched writes the modified XML back into the ZIP via saveDocxZip after validations. If saveDocxZip fails (I/O error, permissions), there is no retry/backout/atomic-replace behavior implemented here. Consider writing to a temporary file then atomically renaming or ensuring the function surfaces enough context to callers to handle partial failures.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 10, 2026

CodeAnt AI finished reviewing your PR.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

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

213-227: ⚠️ Potential issue | 🟠 Major

Potential TypeErrorpages and images accessed without null guards.

Line 215 calls pdfPayload.pages.forEach(...) and line 216 calls page.images.forEach(...) without optional chaining. If result.payload is truthy but pages is missing, or a page lacks images, this throws at runtime. Compare with handleReadFile (line 88) which uses ?.flatMap and ?? [].

🐛 Proposed fix — add optional chaining / fallback
             if (result.isPdf && result.payload) {
                 const pdfPayload = result.payload as { pages: Array<{ text: string; images: Array<{ data: string; mimeType: string }> }> };
-                pdfPayload.pages.forEach((page) => {
-                    page.images.forEach((image) => {
+                (pdfPayload.pages ?? []).forEach((page) => {
+                    (page.images ?? []).forEach((image) => {
                         contentItems.push({
                             type: "image",
                             data: image.data,
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 88: Update the package.json dependency "@xmldom/xmldom" from "^0.8.11" to
"^0.9.8" to pick up the latest stable release, then run your package manager
(npm install or yarn install) to update node_modules and the lockfile
(package-lock.json or yarn.lock); after updating, run the test suite and any
build/CI checks to ensure compatibility and commit the updated package.json and
lockfile.

In `@src/tools/docx/dom.ts`:
- Around line 17-19: The parseXml function currently calls new
DOMParser().parseFromString(...) which lets `@xmldom/xmldom` emit non-fatal parse
errors to console and return a partial DOM; update parseXml to instantiate
DOMParser with an errorHandler option (capture warnings/errors) and/or inspect
the returned Document for parsererror nodes, collect any non-fatal messages, and
throw a descriptive Error when any parse issues are detected so callers (e.g.,
getBody()) fail fast; refer to the parseXml function and the DOMParser from
`@xmldom/xmldom` when making this change.

In `@src/tools/docx/modify.ts`:
- Around line 270-284: The current branch that handles idx === paragraphs.length
uses body.appendChild(newP) which can append after a w:sectPr and produce a
malformed DOCX; instead, detect if body contains a final w:sectPr element and
insert newP before that element (fall back to appendChild only if no w:sectPr
exists). Update the insertion logic in the block around body.appendChild/newP
(references: body, newP, paragraphs, nodeListToArray) to search body.childNodes
for an Element with nodeName 'w:sectPr' and call body.insertBefore(newP,
sectPrNode) when found.

In `@src/tools/filesystem.ts`:
- Around line 958-980: The code currently uses a fragile content-sniffing
heuristic (content.trim().startsWith('<w:body')) to decide between creating a
DOCX and calling replaceBodyXml, which can misinterpret plain text that begins
with "<w:body"; change the API to make intent explicit by adding an explicit
mode/flag (e.g., mode: 'create' | 'replaceBody') to the function that accepts
content and use that flag to choose between calling replaceBodyXml(validPath,
targetPath, trimmedContent) and writeDocxImpl(targetPath, content) (maintaining
validatePath usage), or alternatively perform robust XML validation (parse
trimmedContent and verify it is well-formed XML with a w:body root) before
taking the replaceBodyXml path; update all callers to pass the new mode or
ensure content is validated accordingly.

In `@src/tools/schemas.ts`:
- Around line 118-122: The ReplaceParagraphAtBodyIndexOpSchema currently uses
bodyChildIndex: z.number(), which permits fractional and negative values causing
runtime TypeError in replace-paragraph-at-body-index.ts; update the schema to
enforce a non-negative integer (e.g., use z.number().int().nonnegative() or
equivalent) for the bodyChildIndex field so invalid fractional/negative indices
are rejected at parse time; keep the existing runtime bounds check in
replace-paragraph-at-body-index.ts for upper-bound validation.

In `@src/utils/files/base.ts`:
- Line 233: DocxFileHandler.getInfo currently returns fileType: 'binary' which
contradicts the updated fileType union that added 'docx'; update the
DocxFileHandler.getInfo method to return fileType: 'docx' (instead of 'binary')
and ensure any related metadata or callers expecting 'binary' are adjusted if
needed so the new 'docx' variant is used consistently (reference:
DocxFileHandler.getInfo and the fileType union in base.ts).

In `@src/utils/files/docx.ts`:
- Around line 149-159: The returned FileInfo object in the docx processor
currently sets fileType to 'binary'; update it to 'docx' so consumers can
correctly detect DOCX files—locate the return object in the function in
src/utils/files/docx.ts (the block that returns
size/created/modified/accessed/isDirectory/isFile/permissions/fileType/metadata)
and change fileType: 'binary' to fileType: 'docx'.
- Around line 93-107: The parsed JSON result is not validated and may be a
non-array value assigned to modifications; update the logic in the function
handling `content` (the branch that calls JSON.parse) so that after parsing you
check Array.isArray(parsed) and only assign to `modifications` when
true—otherwise fall back to treating the input as a single replace operation (as
the existing string fallback does) or throw a clear error; reference the
`content` variable and the `modifications` array so you locate the JSON.parse
branch and ensure `modifyDocxContent` receives an actual array of
DocxModification objects.
🧹 Nitpick comments (13)
src/tools/docx/zip.ts (1)

35-43: saveDocxZip mutates the passed zip instance in place.

Line 40 modifies the caller's zip object before writing. If a caller ever needs to save multiple variants or retry with different XML, this side effect could surprise. Consider documenting this or cloning before mutation. Low risk given current usage.

src/tools/filesystem.ts (1)

950-997: writeDocx lacks telemetry capture() calls, unlike writePdf.

writePdf (Lines 897–930) logs telemetry for both creation and modification modes. writeDocx has no capture() calls, creating an observability gap. Consider adding similar telemetry for consistency.

Example telemetry addition
     if (typeof content === 'string') {
         const trimmedContent = content.trim();
         if (trimmedContent.startsWith('<w:body')) {
+            capture('server_write_docx', {
+                fileExtension: '.docx',
+                mode: 'replaceBody',
+                contentLength: trimmedContent.length
+            });
             const targetPath = outputPath ? await validatePath(outputPath) : validPath;
src/tools/docx/dom.ts (2)

148-169: createElement used for namespaced OOXML elements instead of createElementNS.

Lines 154, 164 (and similar patterns in styleParagraphRuns) use doc.createElement('w:rPr') rather than doc.createElementNS(nsUri, 'w:rPr'). In @xmldom/xmldom, this works in practice because the prefix w: is already declared in the document and xmldom preserves it. However, it produces elements without a proper namespace binding, which could cause issues with stricter XML tooling downstream.

This is low risk for DOCX files consumed by Word/LibreOffice (they're lenient), but worth being aware of.


126-137: setParagraphTextMinimal silently no-ops on paragraphs with no <w:t> nodes.

If a paragraph has runs but no text nodes (e.g., image-only or field-code paragraphs), this function returns without any indication that the replacement didn't happen. Callers relying on this for text replacement won't know it was skipped.

Consider returning a boolean to indicate success, or at minimum, documenting this behavior at the call sites.

src/tools/docx/ops/set-color-for-paragraph-exact.ts (1)

16-16: No validation on op.color — invalid hex values will produce invalid OOXML.

If an invalid color string (e.g., "red" instead of "FF0000") is passed, it will be written as w:val="red" which is not a valid OOXML color value. This won't cause a runtime error here but could produce a document that Word flags as corrupted.

Consider validating at the op dispatch level or in the schema.

src/tools/schemas.ts (1)

124-134: Consider validating color as a hex string.

Both color op schemas accept any string for color. A simple regex constraint would catch obvious typos early and provide a better error message than silent DOM corruption.

Proposed fix
+const hexColor = z.string().regex(/^[0-9A-Fa-f]{6}$/, 'Expected 6-digit hex color (e.g. "FF0000")');
+
 const SetColorForStyleOpSchema = z.object({
   type: z.literal('set_color_for_style'),
   style: z.string(),
-  color: z.string(),
+  color: hexColor,
 });
 
 const SetColorForParagraphExactOpSchema = z.object({
   type: z.literal('set_color_for_paragraph_exact'),
   text: z.string(),
-  color: z.string(),
+  color: hexColor,
 });
src/handlers/filesystem-handlers.ts (2)

414-426: Dynamic import is fine but lacks timeout protection.

handleReadDocx uses await import(...) which is acceptable for code-splitting. However, unlike handleReadFile (which wraps in withTimeout), this handler has no timeout. For large DOCX files, readDocxOutline could be slow.


435-439: parsed.ops as any bypasses type safety.

WriteDocxArgsSchema validates ops through Zod, but the inferred type may not align with the DocxOp[] expected by writeDocxPatched. Consider narrowing the type explicitly instead of casting to any.

♻️ Suggested improvement
-        const result = await writeDocxPatched(
-            parsed.inputPath,
-            parsed.outputPath,
-            parsed.ops as any
-        );
+        const result = await writeDocxPatched(
+            parsed.inputPath,
+            parsed.outputPath,
+            parsed.ops as DocxOp[]
+        );

This requires importing DocxOp from the types module, but preserves intent while being more explicit.

src/tools/docx/modify.ts (2)

112-150: Unused temp files — dead code in replaceBodyXml.

tempDocxPath and tempXmlPath are written (lines 118, 121) but never read back. They appear to be leftover backup/debug artifacts. They add unnecessary I/O and temp file management overhead.

Also, line 128 (documentElement.firstChild as Element) is fragile — if newBodyXml has leading whitespace, firstChild would be a text node and the cast silently produces an element with nodeName === '#text', caught only by the subsequent check. Consider using a more robust child selection.

♻️ Proposed cleanup
 export async function replaceBodyXml(
     inputPath: string,
     outputPath: string,
     newBodyXml: string,
 ): Promise<void> {
-    const tempDir = os.tmpdir();
-    const tempDocxPath = path.join(tempDir, `docx_temp_${Date.now()}_${Math.random().toString(36).substring(7)}.docx`);
-    const tempXmlPath = path.join(tempDir, `docx_dom_${Date.now()}_${Math.random().toString(36).substring(7)}.xml`);
-
-    try {
-        const inputBuf = await fs.readFile(inputPath);
-        await fs.writeFile(tempDocxPath, inputBuf);
-
-        const { zip, dom, body } = parseDocument(inputBuf);
-        await fs.writeFile(tempXmlPath, zip.file('word/document.xml')!.asText());
-
-        // Parse the new body XML
-        const newBodyDom = new DOMParser().parseFromString(
-            `<root>${newBodyXml}</root>`,
-            'application/xml',
-        );
-        const newBodyElement = newBodyDom.documentElement.firstChild as Element;
-        if (!newBodyElement || newBodyElement.nodeName !== 'w:body') {
-            throw new Error('Invalid body XML: must start with <w:body>');
-        }
+    const inputBuf = await fs.readFile(inputPath);
+    const { zip, dom, body } = parseDocument(inputBuf);
+
+    // Parse the new body XML
+    const newBodyDom = new DOMParser().parseFromString(
+        `<root>${newBodyXml}</root>`,
+        'application/xml',
+    );
+    const children = nodeListToArray(newBodyDom.documentElement.childNodes);
+    const newBodyElement = children.find(
+        (n) => n.nodeType === 1 && (n as Element).nodeName === 'w:body',
+    ) as Element | undefined;
+    if (!newBodyElement) {
+        throw new Error('Invalid body XML: must contain <w:body>');
+    }

This removes the unused temp files and makes the child lookup robust to whitespace text nodes.


161-225: writeDocx creates a minimal DOCX — consider xml:space="preserve" on <w:t>.

The generated <w:t> elements (line 188) don't include xml:space="preserve", which means leading/trailing spaces in paragraph text will be stripped by conforming OOXML readers.

♻️ Suggested fix
-        (para) => `    <w:p>
+        (para) => `    <w:p>
       <w:r>
-        <w:t>${escaped(para)}</w:t>
+        <w:t xml:space="preserve">${escaped(para)}</w:t>
       </w:r>
     </w:p>`,
src/utils/files/docx.ts (1)

9-9: Unused import: writeDocx.

writeDocx is imported but never used in this file — the write method throws unconditionally. Remove the import to avoid confusion.

♻️ Proposed fix
-import { readDocx, getDocxMetadata, modifyDocxContent, writeDocx } from '../../tools/docx/index.js';
+import { readDocx, getDocxMetadata, modifyDocxContent } from '../../tools/docx/index.js';
src/tools/docx/read.ts (2)

183-224: readDocx reads the DOCX file from disk 3 times.

loadDocx(path) is called at line 192, then getDocxMetadata(path) at line 220 internally calls both loadDocx(path) and extractParagraphs(path) (which calls loadDocx(path) again). For a large DOCX this triples I/O. Additionally, extractParagraphs re-extracts the same paragraph list that readDocx already computed as allParagraphs.

Consider refactoring to pass the already-loaded zip and parsed paragraphs into a metadata builder rather than re-reading from disk.


86-132: Significant duplication across extractTextFromDocx, extractParagraphs, and readDocx.

All three functions repeat the same pattern: load zip → get word/document.xml → parse DOM → get body → iterate w:p children. Consider extracting a shared internal helper that returns the parsed body (or a loaded { zip, body } tuple) to DRY up the code.

"@opendocsg/pdf2md": "^0.2.2",
"@supabase/supabase-js": "^2.89.0",
"@vscode/ripgrep": "^1.15.9",
"@xmldom/xmldom": "^0.8.11",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

npm view `@xmldom/xmldom` versions --json | tail -20

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 357


🏁 Script executed:

npm view pizzip versions --json | tail -20

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 265


🏁 Script executed:

npm view `@xmldom/xmldom`@0.8.11 --json | jq '{version: .version, published: .time, license: .license}'

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2200


🏁 Script executed:

npm view pizzip@3.2.0 --json | jq '{version: .version, published: .time, license: .license}'

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 938


🌐 Web query:

@xmldom/xmldom security vulnerabilities CVE

💡 Result:

Known @xmldom/xmldom security CVEs (with affected & fixed versions):

CVE Issue Affected versions (@xmldom/xmldom) Patched / fixed in
CVE-2021-32796 Improper escaping during XML serialization can cause unexpected syntactic changes < 0.7.0 (xmldom ≤ 0.6.0) 0.7.0 (and later) [1]
CVE-2022-37616 Prototype pollution in copy() (maintainer disputes/considers report invalid, but CVE exists) < 0.7.6, >= 0.8.0 < 0.8.3, = 0.9.0-beta.1 0.7.6, 0.8.3, >= 0.9.0-beta.2 [2][3]
CVE-2022-39353 Parser accepts XML with multiple top-level elements (multiple roots) without error < 0.7.7, >= 0.8.0 < 0.8.4, >= 0.9.0-beta.1 < 0.9.0-beta.4 0.7.7, 0.8.4, >= 0.9.0-beta.4 [4][5]

Practical guidance: upgrade to a version that is at least the “patched/fixed in” version for each CVE above (ideally a version that satisfies all three). [1][3][5]

Sources: NVD CVE records and GitHub Security Advisories for xmldom/xmldom. [1][2][3][4][5]


🌐 Web query:

pizzip security vulnerabilities CVE

💡 Result:

As of February 10, 2026, there are no publicly listed CVEs / direct security advisories for the npm package pizzip in major vulnerability databases (e.g., Snyk reports “No direct vulnerabilities” for the latest release). [1]

Notes:

  • pizzip is the synchronous ZIP library from open-xml-templating/pizzip. [2]
  • Even if pizzip itself has no CVE, your app can still be affected by indirect vulnerabilities in its dependency chain (Snyk notes this distinction). [1]

Sources

  1. Snyk vulnerability page for pizzip (no known direct issues): (security.snyk.io)
  2. pizzip upstream repository (project identity/ownership): (github.com)

Citations:


Consider upgrading @xmldom/xmldom from 0.8.11 to the latest stable 0.9.8.

@xmldom/xmldom 0.8.11 is free of known vulnerabilities (all relevant CVEs are patched), but the latest stable release is 0.9.8 (Feb 2025). Since the caret constraint allows 0.9.x versions, upgrading is recommended for access to recent improvements and fixes. pizzip 3.2.0 is already the latest stable version and has no known vulnerabilities.

🤖 Prompt for AI Agents
In `@package.json` at line 88, Update the package.json dependency "@xmldom/xmldom"
from "^0.8.11" to "^0.9.8" to pick up the latest stable release, then run your
package manager (npm install or yarn install) to update node_modules and the
lockfile (package-lock.json or yarn.lock); after updating, run the test suite
and any build/CI checks to ensure compatibility and commit the updated
package.json and lockfile.

Comment on lines +17 to +19
export function parseXml(xmlStr: string): Document {
return new DOMParser().parseFromString(xmlStr, 'application/xml');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat package.json | grep -A 5 -B 5 xmldom

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 424


🌐 Web query:

@xmldom/xmldom DOMParser errorHandler parse errors behavior

💡 Result:

What DOMParser “parse errors” do in @xmldom/xmldom

1) Legacy behavior (errorHandler) — mostly reporting, not stopping

Historically (and still supported for compatibility), DOMParser accepts a non‑standard errorHandler option that can be either:

  • an object with warning, error, fatalError callbacks, or
  • a function (level, msg) => ... (callback model).
    The parser invokes these callbacks when it encounters issues, but parsing often continues and a Document may still be returned, depending on what happened. (npmjs.com)

2) Current behavior (0.9.x) — onError + consistent ParseError throwing

In @xmldom/xmldom 0.9.0+, error handling was redesigned to be more consistent. The preferred option is now onError(level, message, context), where level is one of warning | error | fatalError. (tessl.io)

Key change: fatal parsing problems now throw a ParseError (and stop parsing), instead of quietly producing a broken/incomplete Document. (dev.to)

Also: errorHandler still exists but is deprecated in favor of onError. (tessl.io)

3) How to make parsing “strict” (fail fast)

@xmldom/xmldom provides predefined handlers you can plug into onError:

  • onErrorStopParsing: throw on error and fatalError
  • onWarningStopParsing: throw on warning too (strictest)
    (tessl.io)
import { DOMParser, onErrorStopParsing, onWarningStopParsing } from '@xmldom/xmldom';

// fail on any error (error or fatalError)
const strict = new DOMParser({ onError: onErrorStopParsing });

// fail even on warnings
const veryStrict = new DOMParser({ onError: onWarningStopParsing });

If you tell me which @xmldom/xmldom version you’re on and what input you’re parsing (text/xml vs text/html), I can describe the exact outcome you should expect for your specific parse error.

Citations:


🏁 Script executed:

rg "parseXml" src/ -A 3 -B 1

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1016


🏁 Script executed:

# Check if there's any error handling or validation downstream
rg "parseError|ParseError|Error.*parse|parse.*error" src/tools/docx/ -i

# Check if document is validated after parsing
rg "getBody|getDocumentXml" src/tools/docx/ -A 5 -B 2 | head -60

# Look at the full write.ts usage context
sed -n '1,50p' src/tools/docx/write.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 4765


🌐 Web query:

@xmldom/xmldom 0.8.11 DOMParser error handling behavior default

💡 Result:

In @xmldom/xmldom 0.8.11, if you call:

new DOMParser().parseFromString(xml, 'text/xml')

and you don’t pass an options.errorHandler, the parser uses the built-in DOMHandler as the default error handler (because the default domBuilder is a DOMHandler). Default behavior is: warnings go to console.warn, errors go to console.error, and fatalError throws a ParseError (which stops parsing). The default options also include a locator:{} so messages include line/column info. [1]


parseXml does not handle non-fatal XML parse errors from @xmldom/xmldom.

In @xmldom/xmldom 0.8.11, non-fatal parse errors are logged to console and parsing continues, potentially producing an incomplete DOM. While fatal errors do throw, recoverable parse errors would silently slip through and cause cryptic failures downstream (e.g., getBody() throwing unexpectedly if the body element was not fully parsed).

Consider adding error detection to catch parse issues early:

Proposed fix
 export function parseXml(xmlStr: string): Document {
-    return new DOMParser().parseFromString(xmlStr, 'application/xml');
+    const errors: string[] = [];
+    const doc = new DOMParser({
+        errorHandler: {
+            warning: () => {},
+            error: (msg: string) => errors.push(msg),
+            fatalError: (msg: string) => errors.push(msg),
+        },
+    }).parseFromString(xmlStr, 'application/xml');
+    if (errors.length > 0) {
+        throw new Error(`XML parse error: ${errors[0]}`);
+    }
+    return doc;
 }
🤖 Prompt for AI Agents
In `@src/tools/docx/dom.ts` around lines 17 - 19, The parseXml function currently
calls new DOMParser().parseFromString(...) which lets `@xmldom/xmldom` emit
non-fatal parse errors to console and return a partial DOM; update parseXml to
instantiate DOMParser with an errorHandler option (capture warnings/errors)
and/or inspect the returned Document for parsererror nodes, collect any
non-fatal messages, and throw a descriptive Error when any parse issues are
detected so callers (e.g., getBody()) fail fast; refer to the parseXml function
and the DOMParser from `@xmldom/xmldom` when making this change.

Comment on lines +270 to +284
if (idx < 0 || idx > paragraphs.length) return;

if (idx === paragraphs.length) {
body.appendChild(newP);
} else {
let current = 0;
for (const child of nodeListToArray(body.childNodes)) {
if (child.nodeType !== 1 || (child as Element).nodeName !== 'w:p') continue;
if (current === idx) {
body.insertBefore(newP, child);
break;
}
current++;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inserting at end may place paragraph after w:sectPr.

Line 273 uses body.appendChild(newP) when idx === paragraphs.length. In standard DOCX, w:body's last child is typically w:sectPr (section properties). Appending after it can produce a malformed document. The paragraph should be inserted before w:sectPr if it exists.

🐛 Proposed fix
     if (idx === paragraphs.length) {
-        body.appendChild(newP);
+        // Insert before w:sectPr if present (must remain last)
+        const lastChild = body.lastChild;
+        if (lastChild && (lastChild as Element).nodeName === 'w:sectPr') {
+            body.insertBefore(newP, lastChild);
+        } else {
+            body.appendChild(newP);
+        }
     } else {
📝 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 (idx < 0 || idx > paragraphs.length) return;
if (idx === paragraphs.length) {
body.appendChild(newP);
} else {
let current = 0;
for (const child of nodeListToArray(body.childNodes)) {
if (child.nodeType !== 1 || (child as Element).nodeName !== 'w:p') continue;
if (current === idx) {
body.insertBefore(newP, child);
break;
}
current++;
}
}
if (idx < 0 || idx > paragraphs.length) return;
if (idx === paragraphs.length) {
// Insert before w:sectPr if present (must remain last)
const lastChild = body.lastChild;
if (lastChild && (lastChild as Element).nodeName === 'w:sectPr') {
body.insertBefore(newP, lastChild);
} else {
body.appendChild(newP);
}
} else {
let current = 0;
for (const child of nodeListToArray(body.childNodes)) {
if (child.nodeType !== 1 || (child as Element).nodeName !== 'w:p') continue;
if (current === idx) {
body.insertBefore(newP, child);
break;
}
current++;
}
}
🤖 Prompt for AI Agents
In `@src/tools/docx/modify.ts` around lines 270 - 284, The current branch that
handles idx === paragraphs.length uses body.appendChild(newP) which can append
after a w:sectPr and produce a malformed DOCX; instead, detect if body contains
a final w:sectPr element and insert newP before that element (fall back to
appendChild only if no w:sectPr exists). Update the insertion logic in the block
around body.appendChild/newP (references: body, newP, paragraphs,
nodeListToArray) to search body.childNodes for an Element with nodeName
'w:sectPr' and call body.insertBefore(newP, sectPrNode) when found.

Comment on lines +958 to +980
if (typeof content === 'string') {
// Check if content is body XML (starts with <w:body)
const trimmedContent = content.trim();
if (trimmedContent.startsWith('<w:body')) {
// --- BODY XML REPLACEMENT MODE ---
// Replace body XML in existing DOCX (preserves all styles and other files)
const targetPath = outputPath ? await validatePath(outputPath) : validPath;

// Check if source file exists
try {
await fs.access(validPath);
} catch {
throw new Error(`Source DOCX file does not exist: ${validPath}. Cannot replace body XML in non-existent file.`);
}

// Replace body XML (preserves all styles and formatting)
await replaceBodyXml(validPath, targetPath, trimmedContent);
} else {
// --- DOCX CREATION MODE ---
// Create new DOCX from text (minimal structure, no styles)
const targetPath = outputPath ? await validatePath(outputPath) : validPath;
await writeDocxImpl(targetPath, content);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Content-sniffing heuristic (<w:body) is fragile and undocumented at the API boundary.

The startsWith('<w:body') check on Line 961 overloads the content: string parameter with two distinct semantics (plain text vs. body XML). If a user ever passes a plain-text string that happens to begin with <w:body, it would silently take the XML replacement path and likely corrupt the document.

Consider either:

  • Using a separate parameter/flag (e.g., mode: 'create' | 'replaceBody' | 'modify') to make intent explicit, or
  • Documenting this convention prominently in the schema/tool description so callers are aware.
🤖 Prompt for AI Agents
In `@src/tools/filesystem.ts` around lines 958 - 980, The code currently uses a
fragile content-sniffing heuristic (content.trim().startsWith('<w:body')) to
decide between creating a DOCX and calling replaceBodyXml, which can
misinterpret plain text that begins with "<w:body"; change the API to make
intent explicit by adding an explicit mode/flag (e.g., mode: 'create' |
'replaceBody') to the function that accepts content and use that flag to choose
between calling replaceBodyXml(validPath, targetPath, trimmedContent) and
writeDocxImpl(targetPath, content) (maintaining validatePath usage), or
alternatively perform robust XML validation (parse trimmedContent and verify it
is well-formed XML with a w:body root) before taking the replaceBodyXml path;
update all callers to pass the new mode or ensure content is validated
accordingly.

Comment on lines +118 to +122
const ReplaceParagraphAtBodyIndexOpSchema = z.object({
type: z.literal('replace_paragraph_at_body_index'),
bodyChildIndex: z.number(),
to: z.string(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

bodyChildIndex should be constrained to a non-negative integer.

z.number() allows fractional values (e.g., 1.5). The runtime bounds check in replace-paragraph-at-body-index.ts (Line 18) would pass for a fractional index within range, but children[1.5] returns undefined in JS, causing a TypeError on child.nodeName.

Proposed fix
 const ReplaceParagraphAtBodyIndexOpSchema = z.object({
   type: z.literal('replace_paragraph_at_body_index'),
-  bodyChildIndex: z.number(),
+  bodyChildIndex: z.number().int().min(0),
   to: z.string(),
 });
🤖 Prompt for AI Agents
In `@src/tools/schemas.ts` around lines 118 - 122, The
ReplaceParagraphAtBodyIndexOpSchema currently uses bodyChildIndex: z.number(),
which permits fractional and negative values causing runtime TypeError in
replace-paragraph-at-body-index.ts; update the schema to enforce a non-negative
integer (e.g., use z.number().int().nonnegative() or equivalent) for the
bodyChildIndex field so invalid fractional/negative indices are rejected at
parse time; keep the existing runtime bounds check in
replace-paragraph-at-body-index.ts for upper-bound validation.


/** File type classification */
fileType: 'text' | 'excel' | 'image' | 'binary';
fileType: 'text' | 'excel' | 'image' | 'binary' | 'docx';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

'docx' added to fileType union but DocxFileHandler.getInfo() returns 'binary'.

The fileType union now includes 'docx', but looking at src/utils/files/docx.ts (Line 155 in the relevant snippet), DocxFileHandler.getInfo() returns fileType: 'binary' instead of fileType: 'docx'. This means the new union variant is never actually used.

Fix in src/utils/files/docx.ts
         return {
             size: stats.size,
             created: stats.birthtime,
             modified: stats.mtime,
             accessed: stats.atime,
             isDirectory: false,
             isFile: true,
             permissions: (stats.mode & 0o777).toString(8),
-            fileType: 'binary',
+            fileType: 'docx',
             metadata
         };
📝 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
fileType: 'text' | 'excel' | 'image' | 'binary' | 'docx';
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: false,
isFile: true,
permissions: (stats.mode & 0o777).toString(8),
fileType: 'docx',
metadata
};
🤖 Prompt for AI Agents
In `@src/utils/files/base.ts` at line 233, DocxFileHandler.getInfo currently
returns fileType: 'binary' which contradicts the updated fileType union that
added 'docx'; update the DocxFileHandler.getInfo method to return fileType:
'docx' (instead of 'binary') and ensure any related metadata or callers
expecting 'binary' are adjusted if needed so the new 'docx' variant is used
consistently (reference: DocxFileHandler.getInfo and the fileType union in
base.ts).

Comment on lines +93 to +107
if (Array.isArray(content)) {
modifications = content;
} else if (typeof content === 'string') {
// Try to parse as JSON
try {
modifications = JSON.parse(content);
} catch {
// If not JSON, treat as single replace operation
modifications = [{
type: 'replace',
findText: range,
replaceText: content
}];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

JSON.parse result not validated as an array.

If a user passes a JSON string that parses to a non-array (e.g., "{}" or "42"), it's assigned directly to modifications: DocxModification[] without validation. This would cause modifyDocxContent to fail in unexpected ways or silently do nothing.

🛡️ Proposed fix — validate parsed JSON is an array
             try {
-                modifications = JSON.parse(content);
+                const parsed = JSON.parse(content);
+                if (!Array.isArray(parsed)) {
+                    throw new Error('not an array');
+                }
+                modifications = parsed;
             } 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 (Array.isArray(content)) {
modifications = content;
} else if (typeof content === 'string') {
// Try to parse as JSON
try {
modifications = JSON.parse(content);
} catch {
// If not JSON, treat as single replace operation
modifications = [{
type: 'replace',
findText: range,
replaceText: content
}];
}
}
if (Array.isArray(content)) {
modifications = content;
} else if (typeof content === 'string') {
// Try to parse as JSON
try {
const parsed = JSON.parse(content);
if (!Array.isArray(parsed)) {
throw new Error('not an array');
}
modifications = parsed;
} catch {
// If not JSON, treat as single replace operation
modifications = [{
type: 'replace',
findText: range,
replaceText: content
}];
}
}
🤖 Prompt for AI Agents
In `@src/utils/files/docx.ts` around lines 93 - 107, The parsed JSON result is not
validated and may be a non-array value assigned to modifications; update the
logic in the function handling `content` (the branch that calls JSON.parse) so
that after parsing you check Array.isArray(parsed) and only assign to
`modifications` when true—otherwise fall back to treating the input as a single
replace operation (as the existing string fallback does) or throw a clear error;
reference the `content` variable and the `modifications` array so you locate the
JSON.parse branch and ensure `modifyDocxContent` receives an actual array of
DocxModification objects.

Comment on lines +149 to +159
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: false,
isFile: true,
permissions: (stats.mode & 0o777).toString(8),
fileType: 'binary',
metadata
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

fileType should be 'docx' instead of 'binary'.

The FileInfo interface (from base.ts) supports 'docx' as a fileType value. Returning 'binary' misclassifies the file and may affect downstream consumers that check fileType.

🐛 Proposed fix
-            fileType: 'binary',
+            fileType: 'docx',
📝 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
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: false,
isFile: true,
permissions: (stats.mode & 0o777).toString(8),
fileType: 'binary',
metadata
};
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: false,
isFile: true,
permissions: (stats.mode & 0o777).toString(8),
fileType: 'docx',
metadata
};
🤖 Prompt for AI Agents
In `@src/utils/files/docx.ts` around lines 149 - 159, The returned FileInfo object
in the docx processor currently sets fileType to 'binary'; update it to 'docx'
so consumers can correctly detect DOCX files—locate the return object in the
function in src/utils/files/docx.ts (the block that returns
size/created/modified/accessed/isDirectory/isFile/permissions/fileType/metadata)
and change fileType: 'binary' to fileType: 'docx'.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 11, 2026

CodeAnt AI is running Incremental review


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

@codeant-ai codeant-ai bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Feb 11, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 11, 2026

CodeAnt AI Incremental review completed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@src/tools/docx/builders/image.ts`:
- Around line 85-112: The buildImageElement function currently hardcodes
wp:docPr id="1" and pic:cNvPr id="0", causing duplicate IDs when multiple images
are inserted; update buildImageElement to accept or compute unique numeric IDs
(e.g., add an optional parameter imageId or pull from a shared monotonically
incrementing counter stored on the document builder) and use that ID for both
the wp:docPr/@id and pic:cNvPr/@id attributes (ensure any callers that create
images supply or obtain the next ID so IDs are globally unique across the
document).
- Around line 78-112: The buildDrawingXml function is duplicated; extract it to
a shared module (e.g., create a new module exporting buildDrawingXml) and update
both callers to import that single export; specifically, move the
buildDrawingXml implementation (the function signature and its helpers like
escapeXmlAttr and NAMESPACES usage) into the new module and replace the local
definitions in src/tools/docx/builders/image.ts and
src/tools/docx/ops/insert-image-after-text.ts with imports of buildDrawingXml so
the hardcoded-ID fix and XML generation logic are maintained in one place.

In `@src/tools/docx/builders/table.ts`:
- Around line 87-95: The header row generation in the table builder can create a
different number of header cells than the table's colCount (which may be derived
from spec.rows[0].length), causing grid/row mismatches; inside the block that
builds headers (referencing spec.headers, buildCell, tbl, tr, and spec.rows)
validate and normalize header length to match the computed colCount: compute
colCount consistently (preferably derive once from spec.colCount ||
spec.headers?.length || spec.rows?.[0]?.length), then when spec.headers exists,
trim or pad the header array to colCount (pad with empty strings) and only
iterate up to colCount when calling buildCell; ensure colWidths indexing also
uses that same colCount to avoid out-of-bounds lookups.
- Around line 97-105: The data row loop in src/tools/docx/builders/table.ts may
produce ragged rows because it iterates by row.length rather than the table's
expected column count; update the block that builds rows (the loop referencing
spec.rows, buildCell, spec.colWidths and tbl) to ensure every w:tr has exactly
the same number of w:tc as the table grid by computing the expected colCount
(from headers or first row) and either pad short rows with empty cells via
buildCell('', false, width) for missing columns (using spec.colWidths[i] where
available) or throw/validate when a row is longer than colCount so the table
grid remains consistent.

In `@src/tools/docx/create.ts`:
- Around line 418-427: The stats calculation uses bodyChildCount =
content.items.length which can be wrong if buildImageElement or other builders
insert multiple nodes or errors occur; instead compute bodyChildrenAfter from
the assembled document body itself (e.g., call getBodyChildren or similar
utility) and set stats.bodyChildrenAfter to that real count; update the stats
object creation near the WriteDocxStats assignment (replace bodyChildCount
usage) and ensure any references to buildImageElement or content.items remain
unchanged except for relying on the actual DOM children count.
- Around line 58-64: The created document.xml.rels currently writes an empty
<Relationships> element; update the code that writes 'document.xml.rels' (the
zip.folder('word')?.folder('_rels')?.file(...) call that produces
document.xml.rels) to include Relationship entries for the parts you add to the
package: add relationships for styles.xml, settings.xml, webSettings.xml, and
fontTable.xml with unique Ids (e.g., rIdStyles, rIdSettings, rIdWebSettings,
rIdFontTable), the correct Type URIs per OOXML (styles, settings, webSettings,
fonts) and Target attributes pointing to their respective files (e.g.,
'styles.xml', 'settings.xml', 'webSettings.xml', 'fontTable.xml') so Word can
discover and apply those parts.

In `@src/tools/docx/ops/header-replace-text-exact.ts`:
- Around line 26-29: Remove the dead guard and unused variable around
zip.folder('word'): the call zip.folder('word') always returns a PizZip instance
so the check if (!files) and the early return ('no_word_folder') must be
deleted; instead rely on the existing discovery logic that iterates
Object.keys(zip.files) and filters by /^word\/header[0-9]+\.xml$/ in the
headerReplaceTextExact flow, and remove the unused files binding to keep the
function concise.

In `@src/tools/docx/ops/insert-image-after-text.ts`:
- Around line 40-46: The current anchor validation in insert-image-after-text.ts
silently prefers `before` when both `op.before` and `op.after` are set; update
the validation to detect the ambiguous case and return a skipped result instead
of proceeding. Specifically, in the block that computes `anchorText` and
`position` (using `op.before ?? op.after` and `op.before ? 'before' : 'after'`),
first check if both `op.before` and `op.after` are truthy and if so return `{
op, status: 'skipped', matched: 0, reason: 'ambiguous_anchor: provide exactly
one of "after" or "before"' }`; otherwise continue to set `anchorText` and
`position` as before. Ensure you reference `op.before`, `op.after`,
`anchorText`, and `position` when making the change.

In `@src/tools/docx/ops/insert-paragraph-after-text.ts`:
- Around line 22-43: The empty-document branch in insert-paragraph-after-text.ts
currently returns { op, status: 'applied', matched: 0 } which conflicts with the
convention that matched: 0 indicates skipped; update the return to { op, status:
'applied', matched: 1, reason: 'empty_document_append' } (or otherwise set
matched to a non-zero value) so the creation of the paragraph is unambiguously
reported by the function that uses op, body, children and newP/newR/newT.

In `@src/tools/schemas.ts`:
- Around line 241-257: The refinement on WriteDocxArgsSchema currently only
enforces presence of ops for updates but doesn't forbid content when inputPath
is present; update the refine predicate for WriteDocxArgsSchema so that: when
inputPath is absent require content exists and ops must be undefined/absent, and
when inputPath is present require ops exists and content must be
undefined/absent; also adjust the refinement error message to reflect both
presence and mutual exclusion rules (mention inputPath, content, and ops) to
make the contract explicit.
- Around line 174-192: The schemas InsertTableOpSchema and InsertImageOpSchema
currently allow both after and before to be provided, which contradicts the
JSDoc; update each schema to enforce mutual exclusivity by adding a Zod
refinement (e.g., .refine(obj => !(obj.after && obj.before), { message: 'Provide
either "after" or "before", not both', path: ['after','before'] })) to validate
that both fields are not present at the same time for InsertTableOpSchema and
InsertImageOpSchema.
🧹 Nitpick comments (9)
src/tools/docx/relationships.ts (1)

11-12: Nit: Consolidate the two imports from ./constants.js into one.

Suggested fix
-import { getMimeType } from './constants.js';
-import { DOCX_PATHS, NAMESPACES } from './constants.js';
+import { getMimeType, DOCX_PATHS, NAMESPACES } from './constants.js';
src/tools/docx/ops/insert-image-after-text.ts (1)

35-39: Synchronous fs usage blocks the event loop and is inconsistent with builders/image.ts.

This file uses fs.existsSync (Line 50) and fs.readFileSync (Line 67) while the corresponding buildImageElement in builders/image.ts uses fs/promises (async). If this function is ever called from an async context, the sync reads will block. Consider making applyInsertImage async (returning Promise<OpResult>) to align with the rest of the image-handling code. This would also require updating the op dispatcher.

Also applies to: 23-23

src/tools/docx/ops/insert-paragraph-after-text.ts (1)

27-40: Paragraph-building logic is duplicated between the empty-doc and normal-match branches.

The w:p + optional w:pPr/w:pStyle + w:r/w:t construction at lines 27–40 is nearly identical to lines 55–71. Extracting a small helper (e.g., buildParagraph(doc, text, style?)) would reduce duplication and make future changes less error-prone.

Also applies to: 55-71

src/tools/docx/ops/insert-table-after-text.ts (1)

30-31: Mutual exclusivity of before/after is not enforced in this function.

The type contract says exactly one of before or after must be provided. If both are supplied, anchorText silently uses op.before (via ??) while position uses the truthiness of op.before. This works by accident but relies entirely on upstream schema validation. Consider adding an explicit guard:

Proposed defensive guard
+    if (op.before && op.after) {
+        return { op, status: 'skipped', matched: 0, reason: 'provide_only_one_of_before_or_after' };
+    }
+
     const anchorText = op.before ?? op.after;
     const position: 'before' | 'after' = op.before ? 'before' : 'after';
src/tools/docx/index.ts (1)

10-36: Two separate type export blocks from the same module with identical // Types comments.

Both blocks export from ./types.js with the same // Types comment. Consider either merging them into a single block or differentiating the comments (e.g., // Content types vs // Operational types).

src/tools/docx/create.ts (2)

103-275: Heading styles 4–9 are nearly identical — consider generating them programmatically.

Headings 4–9 share the same font size (22), color (1F3763), and formatting — differing only in outlineLvl. This is ~170 lines of repetitive XML. Generating them in a loop would reduce maintenance burden and risk of copy-paste drift.

That said, since this is a static template and correctness is straightforward, this is purely a maintainability suggestion.


396-410: Content items with unrecognized type values are silently ignored.

If content.items contains an item with a type other than 'paragraph', 'table', or 'image', it's skipped without any warning. This could lead to confusing results where the user provides content that doesn't appear in the output.

Consider adding a warning or throwing for unknown item types:

Proposed enhancement
         } else if (item.type === 'image') {
             const imgP = await buildImageElement(doc, zip, item);
             body.appendChild(imgP);
+        } else {
+            // Type system should prevent this, but guard against runtime surprises
+            throw new Error(`Unknown content item type: ${(item as any).type}`);
         }
src/handlers/filesystem-handlers.ts (2)

442-455: as any casts bypass type safety on ops and content.

Lines 445 and 454 cast the Zod-parsed output to any, losing the type contract between the schema and the downstream functions (writeDocxPatched, createDocxNew). If the schema drifts from the DocxOp[] / DocxContentStructure interfaces, the mismatch won't be caught at compile time.

Consider using Zod's inferred types or explicit type assertions to maintain safety:

Suggested approach
-            result = await writeDocxPatched(
-                parsed.inputPath,
-                parsed.outputPath,
-                parsed.ops as any
-            );
+            result = await writeDocxPatched(
+                parsed.inputPath,
+                parsed.outputPath,
+                parsed.ops as DocxOp[]
+            );
-            result = await createDocxNew(
-                parsed.outputPath,
-                parsed.content as any
-            );
+            result = await createDocxNew(
+                parsed.outputPath,
+                parsed.content as DocxContentStructure
+            );

This requires importing DocxOp and DocxContentStructure types, but preserves compile-time checking.


111-133: DOCX handling in read_file is functional but the response text is a single very long template literal.

Line 129 builds the entire response (XML + metadata + instructions) in one string. This works but is hard to read and maintain. Consider composing it from smaller parts.

Also, the guidance at the end ("To modify this DOCX: 1. Edit the body XML...") references write_docx but the tool description for read_file (in server.ts, line 309) already tells users to prefer read_docx for editing workflows. This creates slightly conflicting guidance — one path says "use read_docx + write_docx" while this response says "edit the body XML and use write_docx". This may confuse the LLM.

Comment on lines +78 to +112
function buildDrawingXml(
rId: string,
widthEmu: number,
heightEmu: number,
altText: string,
fileName: string,
): string {
return (
`<w:drawing xmlns:w="${NAMESPACES.W}">` +
`<wp:inline distT="0" distB="0" distL="0" distR="0" ` +
`xmlns:wp="${NAMESPACES.WP}">` +
`<wp:extent cx="${widthEmu}" cy="${heightEmu}"/>` +
`<wp:docPr id="1" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
`<a:graphic xmlns:a="${NAMESPACES.A}">` +
`<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">` +
`<pic:pic xmlns:pic="${NAMESPACES.PIC}">` +
`<pic:nvPicPr>` +
`<pic:cNvPr id="0" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
`<pic:cNvPicPr/>` +
`</pic:nvPicPr>` +
`<pic:blipFill>` +
`<a:blip r:embed="${rId}" xmlns:r="${NAMESPACES.R}"/>` +
`<a:stretch><a:fillRect/></a:stretch>` +
`</pic:blipFill>` +
`<pic:spPr>` +
`<a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm>` +
`<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` +
`</pic:spPr>` +
`</pic:pic>` +
`</a:graphicData>` +
`</a:graphic>` +
`</wp:inline>` +
`</w:drawing>`
);
}
Copy link
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

🏁 Script executed:

#!/bin/bash
# Find all occurrences of buildDrawingXml
rg -n 'buildDrawingXml' --type=ts -A 2

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Read the buildDrawingXml function from builders/image.ts
echo "=== src/tools/docx/builders/image.ts (lines 78-115) ==="
sed -n '78,115p' src/tools/docx/builders/image.ts

echo ""
echo "=== src/tools/docx/ops/insert-image-after-text.ts (lines 129-165) ==="
sed -n '129,165p' src/tools/docx/ops/insert-image-after-text.ts

# Check for comments around the second function mentioning duplication
echo ""
echo "=== Checking context around line 129 in insert-image-after-text.ts ==="
sed -n '120,130p' src/tools/docx/ops/insert-image-after-text.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 3177


Extract buildDrawingXml to a shared module to eliminate duplication.

The function is duplicated verbatim in both src/tools/docx/builders/image.ts (lines 78–112) and src/tools/docx/ops/insert-image-after-text.ts (lines 129–163). The comment in the latter file already acknowledges this duplication ("shared with builders/image.ts"). Create a shared module (e.g., builders/drawing.ts or builders/utils.ts) to define this function once and import it from both locations. This centralizes the hardcoded-ID fix in one place and reduces maintenance overhead.

🤖 Prompt for AI Agents
In `@src/tools/docx/builders/image.ts` around lines 78 - 112, The buildDrawingXml
function is duplicated; extract it to a shared module (e.g., create a new module
exporting buildDrawingXml) and update both callers to import that single export;
specifically, move the buildDrawingXml implementation (the function signature
and its helpers like escapeXmlAttr and NAMESPACES usage) into the new module and
replace the local definitions in src/tools/docx/builders/image.ts and
src/tools/docx/ops/insert-image-after-text.ts with imports of buildDrawingXml so
the hardcoded-ID fix and XML generation logic are maintained in one place.

Comment on lines +85 to +112
return (
`<w:drawing xmlns:w="${NAMESPACES.W}">` +
`<wp:inline distT="0" distB="0" distL="0" distR="0" ` +
`xmlns:wp="${NAMESPACES.WP}">` +
`<wp:extent cx="${widthEmu}" cy="${heightEmu}"/>` +
`<wp:docPr id="1" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
`<a:graphic xmlns:a="${NAMESPACES.A}">` +
`<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">` +
`<pic:pic xmlns:pic="${NAMESPACES.PIC}">` +
`<pic:nvPicPr>` +
`<pic:cNvPr id="0" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
`<pic:cNvPicPr/>` +
`</pic:nvPicPr>` +
`<pic:blipFill>` +
`<a:blip r:embed="${rId}" xmlns:r="${NAMESPACES.R}"/>` +
`<a:stretch><a:fillRect/></a:stretch>` +
`</pic:blipFill>` +
`<pic:spPr>` +
`<a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm>` +
`<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` +
`</pic:spPr>` +
`</pic:pic>` +
`</a:graphicData>` +
`</a:graphic>` +
`</wp:inline>` +
`</w:drawing>`
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded wp:docPr id="1" will produce duplicate IDs when multiple images are inserted.

The OOXML spec requires unique id values for wp:docPr across the document. When buildImageElement is called multiple times, every image gets id="1" (Line 90) and pic:cNvPr id="0" (Line 95). While Word is often tolerant, strict validators and other consumers (LibreOffice, accessibility tools) may reject or misrender the document.

Consider accepting or computing a unique ID (e.g., scan existing docPr IDs in the document, or use a monotonically incrementing counter).

🤖 Prompt for AI Agents
In `@src/tools/docx/builders/image.ts` around lines 85 - 112, The
buildImageElement function currently hardcodes wp:docPr id="1" and pic:cNvPr
id="0", causing duplicate IDs when multiple images are inserted; update
buildImageElement to accept or compute unique numeric IDs (e.g., add an optional
parameter imageId or pull from a shared monotonically incrementing counter
stored on the document builder) and use that ID for both the wp:docPr/@id and
pic:cNvPr/@id attributes (ensure any callers that create images supply or obtain
the next ID so IDs are globally unique across the document).

Comment on lines +87 to +95
// Header row
if (spec.headers && spec.headers.length > 0) {
const tr = doc.createElement('w:tr');
for (let i = 0; i < spec.headers.length; i++) {
const width = spec.colWidths?.[i];
tr.appendChild(buildCell(spec.headers[i], true, width));
}
tbl.appendChild(tr);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Header row cell count could also mismatch colCount when derived from data rows.

If spec.headers is absent, colCount comes from spec.rows[0].length. But if spec.headers is present and its length differs from data row lengths, the grid and rows will be inconsistent. The schema doesn't enforce this alignment.

🤖 Prompt for AI Agents
In `@src/tools/docx/builders/table.ts` around lines 87 - 95, The header row
generation in the table builder can create a different number of header cells
than the table's colCount (which may be derived from spec.rows[0].length),
causing grid/row mismatches; inside the block that builds headers (referencing
spec.headers, buildCell, tbl, tr, and spec.rows) validate and normalize header
length to match the computed colCount: compute colCount consistently (preferably
derive once from spec.colCount || spec.headers?.length ||
spec.rows?.[0]?.length), then when spec.headers exists, trim or pad the header
array to colCount (pad with empty strings) and only iterate up to colCount when
calling buildCell; ensure colWidths indexing also uses that same colCount to
avoid out-of-bounds lookups.

Comment on lines +97 to +105
// Data rows
for (const row of spec.rows) {
const tr = doc.createElement('w:tr');
for (let i = 0; i < row.length; i++) {
const width = spec.colWidths?.[i];
tr.appendChild(buildCell(row[i], false, width));
}
tbl.appendChild(tr);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ragged rows could produce an inconsistent table grid.

Data rows are iterated by their own row.length (line 100), which may differ from colCount (derived from headers or the first row). OOXML expects each w:tr to have the same number of w:tc elements as defined in w:tblGrid. A row with fewer cells could cause rendering issues or corruption in some Word processors.

Consider either padding short rows with empty cells or validating that all rows match the expected column count.

🤖 Prompt for AI Agents
In `@src/tools/docx/builders/table.ts` around lines 97 - 105, The data row loop in
src/tools/docx/builders/table.ts may produce ragged rows because it iterates by
row.length rather than the table's expected column count; update the block that
builds rows (the loop referencing spec.rows, buildCell, spec.colWidths and tbl)
to ensure every w:tr has exactly the same number of w:tc as the table grid by
computing the expected colCount (from headers or first row) and either pad short
rows with empty cells via buildCell('', false, width) for missing columns (using
spec.colWidths[i] where available) or throw/validate when a row is longer than
colCount so the table grid remains consistent.

Comment on lines +58 to +64
// ─── word/_rels/document.xml.rels ────────────────────────────────
zip.folder('word')?.folder('_rels')?.file(
'document.xml.rels',
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>`,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/tools/docx/create.ts | head -150

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 6323


🌐 Web query:

OOXML Office Open XML document.xml.rels relationships styles.xml required specification

💡 Result:

For a WordprocessingML (.docx) package, the specification treats /word/styles.xml (the Style Definitions part) as the target of an implicit relationship from the Main Document part (/word/document.xml). This relationship is stored in the part-relationship item:

  • Relationship part location: /word/_rels/document.xml.rels (relationships for /word/document.xml) (OPC/OOXML packaging model) [4]
  • Relationship Type (Transitional): http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles [1]
  • Relationship Type (Strict): http://purl.oclc.org/ooxml/officeDocument/relationships/styles [2]
  • Typical Target: styles.xml (i.e., /word/styles.xml) [1]

Example document.xml.rels entry (as shown in the spec-derived references):

<Relationship Id="rId3"
  Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
  Target="styles.xml"/>

[1]

Is styles.xml required?

A minimal conformant WordprocessingML package requires only:

  • /[Content_Types].xml
  • /_rels/.rels (package-level relationship)
  • a Main Document part (e.g., /word/document.xml) targeted by that package-level relationship [3]

So, styles.xml is not required for the minimal package, but if a Style Definitions part is present, it is (by definition) the target of an implicit relationship from the Main Document (and/or Glossary Document) part [2]. Also, the Main Document part is explicitly listed as being permitted to have an implicit relationship to the Style Definitions part [5].

Sources: Style Definitions part + relationship type [1][2], minimal required package structure [3], relationship-part location concept [4], Main Document implicit relationships list [5].


🏁 Script executed:

sed -n '65,250p' src/tools/docx/create.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 4828


🏁 Script executed:

sed -n '250,370p' src/tools/docx/create.ts | head -150

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 4008


🏁 Script executed:

sed -n '290,370p' src/tools/docx/create.ts | grep -A 20 "word/_rels/document.xml.rels" || echo "Already viewed"

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 90


Add missing relationship entries in word/_rels/document.xml.rels for created document parts.

The <Relationships> element is empty, but the OOXML specification requires explicit relationship entries for styles.xml, settings.xml, webSettings.xml, and fontTable.xml since these parts are present in the package. Without these entries, Word may not properly recognize and apply the styles, settings, and font definitions defined in the scaffold.

Proposed fix: add required relationship entries
     zip.folder('word')?.folder('_rels')?.file(
         'document.xml.rels',
         `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
+  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
+  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
+  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
 </Relationships>`,
     );
📝 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
// ─── word/_rels/document.xml.rels ────────────────────────────────
zip.folder('word')?.folder('_rels')?.file(
'document.xml.rels',
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>`,
);
// ─── word/_rels/document.xml.rels ────────────────────────────────
zip.folder('word')?.folder('_rels')?.file(
'document.xml.rels',
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
</Relationships>`,
);
🤖 Prompt for AI Agents
In `@src/tools/docx/create.ts` around lines 58 - 64, The created document.xml.rels
currently writes an empty <Relationships> element; update the code that writes
'document.xml.rels' (the zip.folder('word')?.folder('_rels')?.file(...) call
that produces document.xml.rels) to include Relationship entries for the parts
you add to the package: add relationships for styles.xml, settings.xml,
webSettings.xml, and fontTable.xml with unique Ids (e.g., rIdStyles,
rIdSettings, rIdWebSettings, rIdFontTable), the correct Type URIs per OOXML
(styles, settings, webSettings, fonts) and Target attributes pointing to their
respective files (e.g., 'styles.xml', 'settings.xml', 'webSettings.xml',
'fontTable.xml') so Word can discover and apply those parts.

Comment on lines +26 to +29
const files = zip.folder('word');
if (!files) {
return { op, status: 'skipped', matched: 0, reason: 'no_word_folder' };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

PizZip folder() method return value documentation API

💡 Result:

  • PizZip#folder(name: string) returns a new PizZip instance (for chaining) with the created/selected folder as the root. [1]
  • PizZip#folder(regex: RegExp) returns an array of matching folders (empty if none), where each match is a ZipObject. [2]

Sources: [1] [2]


🏁 Script executed:

# Check the file and surrounding context
cat -n src/tools/docx/ops/header-replace-text-exact.ts | head -80

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2971


zip.folder('word') never returns a falsy value — this guard is dead code.

PizZip's folder() method always returns a new PizZip instance regardless of whether the folder exists. The !files check will never be true, making the 'no_word_folder' path unreachable. The files variable itself is also unused afterward — actual discovery happens via Object.keys(zip.files) on line 33. This is harmless since the regex filter on line 34 handles the absence of header files correctly.

Suggested cleanup
-    // Iterate over all files in word/ looking for header*.xml
-    const files = zip.folder('word');
-    if (!files) {
-        return { op, status: 'skipped', matched: 0, reason: 'no_word_folder' };
-    }
-
     // PizZip file listing
🤖 Prompt for AI Agents
In `@src/tools/docx/ops/header-replace-text-exact.ts` around lines 26 - 29, Remove
the dead guard and unused variable around zip.folder('word'): the call
zip.folder('word') always returns a PizZip instance so the check if (!files) and
the early return ('no_word_folder') must be deleted; instead rely on the
existing discovery logic that iterates Object.keys(zip.files) and filters by
/^word\/header[0-9]+\.xml$/ in the headerReplaceTextExact flow, and remove the
unused files binding to keep the function concise.

Comment on lines +40 to +46
// ── Validate anchor ─────────────────────────────────────────────
const anchorText = op.before ?? op.after;
const position: 'before' | 'after' = op.before ? 'before' : 'after';

if (!anchorText) {
return { op, status: 'skipped', matched: 0, reason: 'no_anchor: provide "after" or "before"' };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

No validation when both after and before are provided simultaneously.

The docstring (Line 10) states "Exactly one of after or before must be provided," but the code silently gives before precedence via the ?? operator when both are set. Consider returning a 'skipped' result with a descriptive reason when both anchors are present.

Suggested fix
     // ── Validate anchor ─────────────────────────────────────────────
+    if (op.before && op.after) {
+        return { op, status: 'skipped', matched: 0, reason: 'provide only one of "after" or "before", not both' };
+    }
+
     const anchorText = op.before ?? op.after;
     const position: 'before' | 'after' = op.before ? 'before' : 'after';
📝 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
// ── Validate anchor ─────────────────────────────────────────────
const anchorText = op.before ?? op.after;
const position: 'before' | 'after' = op.before ? 'before' : 'after';
if (!anchorText) {
return { op, status: 'skipped', matched: 0, reason: 'no_anchor: provide "after" or "before"' };
}
// ── Validate anchor ─────────────────────────────────────────────
if (op.before && op.after) {
return { op, status: 'skipped', matched: 0, reason: 'provide only one of "after" or "before", not both' };
}
const anchorText = op.before ?? op.after;
const position: 'before' | 'after' = op.before ? 'before' : 'after';
if (!anchorText) {
return { op, status: 'skipped', matched: 0, reason: 'no_anchor: provide "after" or "before"' };
}
🤖 Prompt for AI Agents
In `@src/tools/docx/ops/insert-image-after-text.ts` around lines 40 - 46, The
current anchor validation in insert-image-after-text.ts silently prefers
`before` when both `op.before` and `op.after` are set; update the validation to
detect the ambiguous case and return a skipped result instead of proceeding.
Specifically, in the block that computes `anchorText` and `position` (using
`op.before ?? op.after` and `op.before ? 'before' : 'after'`), first check if
both `op.before` and `op.after` are truthy and if so return `{ op, status:
'skipped', matched: 0, reason: 'ambiguous_anchor: provide exactly one of "after"
or "before"' }`; otherwise continue to set `anchorText` and `position` as
before. Ensure you reference `op.before`, `op.after`, `anchorText`, and
`position` when making the change.

Comment on lines +22 to +43
// Special case: if document is empty, append the paragraph
if (children.length === 0) {
const doc = body.ownerDocument;
if (!doc) return { op, status: 'skipped', matched: 0, reason: 'no_owner_document' };

const newP = doc.createElement('w:p');
if (op.style) {
const pPr = doc.createElement('w:pPr');
const pStyle = doc.createElement('w:pStyle');
pStyle.setAttribute('w:val', op.style);
pPr.appendChild(pStyle);
newP.appendChild(pPr);
}
const newR = doc.createElement('w:r');
const newT = doc.createElement('w:t');
newT.setAttribute('xml:space', 'preserve');
newT.textContent = op.text;
newR.appendChild(newT);
newP.appendChild(newR);
body.appendChild(newP);
return { op, status: 'applied', matched: 0, reason: 'empty_document_append' };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

status: 'applied' with matched: 0 is semantically ambiguous.

In the empty-document case, the op returns { status: 'applied', matched: 0 }. Other ops use matched: 0 exclusively with status: 'skipped'. This inconsistency could confuse the orchestrator or validation logic — e.g., if it interprets matched: 0 as "nothing happened."

Consider using matched: 1 here (since the paragraph was created), or documenting this as an intentional special case in the OpResult contract.

🤖 Prompt for AI Agents
In `@src/tools/docx/ops/insert-paragraph-after-text.ts` around lines 22 - 43, The
empty-document branch in insert-paragraph-after-text.ts currently returns { op,
status: 'applied', matched: 0 } which conflicts with the convention that
matched: 0 indicates skipped; update the return to { op, status: 'applied',
matched: 1, reason: 'empty_document_append' } (or otherwise set matched to a
non-zero value) so the creation of the paragraph is unambiguously reported by
the function that uses op, body, children and newP/newR/newT.

Comment on lines +174 to +192
const InsertTableOpSchema = z.object({
type: z.literal('insert_table'),
after: z.string().optional(),
before: z.string().optional(),
headers: z.array(z.string()).optional(),
rows: z.array(z.array(z.string())),
colWidths: z.array(z.number()).optional(),
style: z.string().optional(),
});

const InsertImageOpSchema = z.object({
type: z.literal('insert_image'),
after: z.string().optional(),
before: z.string().optional(),
imagePath: z.string(),
width: z.number().optional(),
height: z.number().optional(),
altText: z.string().optional(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

InsertTableOp and InsertImageOp don't enforce mutual exclusivity of after/before.

The type JSDoc states these are mutually exclusive, but the schemas accept both simultaneously. If a caller provides both after and before, the runtime behavior is undefined — whichever the op handler checks first wins silently.

Proposed fix (InsertTableOpSchema example — same for InsertImageOpSchema)
 const InsertTableOpSchema = z.object({
   type: z.literal('insert_table'),
   after: z.string().optional(),
   before: z.string().optional(),
   headers: z.array(z.string()).optional(),
   rows: z.array(z.array(z.string())),
   colWidths: z.array(z.number()).optional(),
   style: z.string().optional(),
-});
+}).refine(
+  (data) => !(data.after && data.before),
+  { message: '"after" and "before" are mutually exclusive' },
+);
📝 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 InsertTableOpSchema = z.object({
type: z.literal('insert_table'),
after: z.string().optional(),
before: z.string().optional(),
headers: z.array(z.string()).optional(),
rows: z.array(z.array(z.string())),
colWidths: z.array(z.number()).optional(),
style: z.string().optional(),
});
const InsertImageOpSchema = z.object({
type: z.literal('insert_image'),
after: z.string().optional(),
before: z.string().optional(),
imagePath: z.string(),
width: z.number().optional(),
height: z.number().optional(),
altText: z.string().optional(),
});
const InsertTableOpSchema = z.object({
type: z.literal('insert_table'),
after: z.string().optional(),
before: z.string().optional(),
headers: z.array(z.string()).optional(),
rows: z.array(z.array(z.string())),
colWidths: z.array(z.number()).optional(),
style: z.string().optional(),
}).refine(
(data) => !(data.after && data.before),
{ message: '"after" and "before" are mutually exclusive' },
);
const InsertImageOpSchema = z.object({
type: z.literal('insert_image'),
after: z.string().optional(),
before: z.string().optional(),
imagePath: z.string(),
width: z.number().optional(),
height: z.number().optional(),
altText: z.string().optional(),
}).refine(
(data) => !(data.after && data.before),
{ message: '"after" and "before" are mutually exclusive' },
);
🤖 Prompt for AI Agents
In `@src/tools/schemas.ts` around lines 174 - 192, The schemas InsertTableOpSchema
and InsertImageOpSchema currently allow both after and before to be provided,
which contradicts the JSDoc; update each schema to enforce mutual exclusivity by
adding a Zod refinement (e.g., .refine(obj => !(obj.after && obj.before), {
message: 'Provide either "after" or "before", not both', path:
['after','before'] })) to validate that both fields are not present at the same
time for InsertTableOpSchema and InsertImageOpSchema.

Comment on lines +241 to +257
export const WriteDocxArgsSchema = z.object({
inputPath: z.string().optional(),
outputPath: z.string(),
ops: z.array(DocxOpSchema).optional(),
content: DocxContentStructureSchema.optional(),
}).refine(
(data) => {
// For new files (no inputPath): require content, ops optional
// For updates (has inputPath): require ops, content not allowed
if (!data.inputPath) {
return !!data.content; // New file must have content
} else {
return !!data.ops; // Update must have ops
}
},
{ message: 'For new files (no inputPath): provide "content". For updates (has inputPath): provide "ops".' }
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

WriteDocxArgsSchema refinement doesn't reject content on update path.

The comment says "For updates (has inputPath): require ops, content not allowed", but the refinement only checks !!data.ops — it doesn't reject data.content being present. A caller could pass both inputPath + content + ops and the schema would accept it, potentially leading to confusion about which is used.

Proposed fix
   (data) => {
     if (!data.inputPath) {
       return !!data.content;
     } else {
-      return !!data.ops;
+      return !!data.ops && !data.content;
     }
   },
🤖 Prompt for AI Agents
In `@src/tools/schemas.ts` around lines 241 - 257, The refinement on
WriteDocxArgsSchema currently only enforces presence of ops for updates but
doesn't forbid content when inputPath is present; update the refine predicate
for WriteDocxArgsSchema so that: when inputPath is absent require content exists
and ops must be undefined/absent, and when inputPath is present require ops
exists and content must be undefined/absent; also adjust the refinement error
message to reflect both presence and mutual exclusion rules (mention inputPath,
content, and ops) to make the contract explicit.

@wonderwhy-er
Copy link
Owner

PR Review — Issues #4 through #13

Thanks for the work on this — the architecture (op pattern, validation snapshots, builders separation) is solid. However there are several issues that need to be addressed before merge.


🔴 #4 — Sync I/O blocks event loop (insert-image-after-text.ts)

fs.readFileSync (line 72) and fs.existsSync (line 62) in ops/insert-image-after-text.ts block the Node.js event loop. The builder version (builders/image.ts) already uses async fs.readFile and fs.access correctly — please align the op implementation to match. The applyOp dispatcher will need to become async, or this op needs special async handling.


🔴 #5 — No path validation on imagePath (security)

Both ops/insert-image-after-text.ts and builders/image.ts read arbitrary files from disk via imagePath with zero validation against allowedDirectories. A crafted input could embed any file (e.g. /etc/shadow, SSH keys) into a DOCX.

Similarly, handleReadDocx and handleWriteDocx in the handlers do not validate input/output paths against allowed directories — the legacy writeDocx in filesystem.ts calls validatePath, but the new handlers skip it.

Please add validatePath() calls for all file paths in the new handlers.


🔴 #6as any bypasses type safety in handleWriteDocx

result = await writeDocxPatched(parsed.inputPath, parsed.outputPath, parsed.ops as any);
result = await createDocxNew(parsed.outputPath, parsed.content as any);

The Zod schemas and TypeScript types should already be aligned. Using as any defeats the type system and can mask real bugs. Please fix the types to flow through properly.


🟡 #7 — DRY: Duplicate buildDrawingXml

The exact same buildDrawingXml function exists in both ops/insert-image-after-text.ts and builders/image.ts. Please extract to a shared location (e.g. a drawing helper or builders/utils.ts).


🟡 #8 — DRY: Duplicate loadDocx vs loadDocxZip

read.ts has a private loadDocx() that does fs.readFile + new PizZip(). zip.ts already exports loadDocxZip() doing the same thing. modify.ts also does new PizZip(inputBuf) inline. Please consolidate to use loadDocxZip from zip.ts everywhere.


🟡 #9 — DRY: Repeated XML parsing pattern

The outline reader, legacy reader, text extractor, and extractParagraphs all repeat nearly identical loops: parse XML → get body → iterate w:body children. Some consolidation would reduce maintenance burden.


🟡 #10replaceBodyXml creates unnecessary temp files

In modify.ts, replaceBodyXml() creates temp files (tempDocxPath, tempXmlPath) and writes to them, but never reads them back. This is dead code creating unnecessary filesystem artifacts. Please remove the temp file logic.


🟡 #11readDocx re-reads file 3x for metadata

readDocx() calls getDocxMetadata(path) which internally calls loadDocx(path) again, and extractParagraphs(path) which also calls loadDocx(path). So a single read loads and parses the ZIP three times. Please pass the already-loaded zip/paragraphs through instead.


🟡 #12docPr id="1" hardcoded in image drawing XML

Both image builders hardcode <wp:docPr id="1">. In a document with multiple images this creates duplicate IDs, which can cause rendering issues in some Word implementations. Please use an incrementing counter or generate unique IDs.


🟡 #13InsertTableOp / InsertImageOp — no after/before mutual exclusivity enforcement

The types document "Mutually exclusive after/before" but neither the Zod schema nor runtime code validates that exactly one is provided. If both are given, before silently wins. If neither is given, it silently skips with no clear error. Please add a .refine() on the Zod schema to enforce exactly one.


🟡 Bonus — read_file offset/length partially broken for DOCX

readDocx() correctly slices paragraphs[] and text by offset/length, but bodyXml always returns the full unsliced body XML. Since DocxFileHandler.read() returns bodyXml as content, offset/length is effectively ignored in the read_file path. The extracted text reference shows a subset while the body XML shows everything — inconsistent behavior. Negative offsets (tail) are also not supported unlike other handlers.


Please address the 🔴 items as blockers and the 🟡 items before or shortly after merge. Happy to re-review once updated.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 12, 2026

CodeAnt AI is running Incremental review


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

@codeant-ai codeant-ai bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Feb 12, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 12, 2026

CodeAnt AI Incremental review completed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@src/tools/docx/dom.ts`:
- Around line 227-238: setParagraphTextMinimal currently silently no-ops when
there are no <w:t> nodes; change its signature to return a boolean (true if text
was actually set, false otherwise), have it return false when tNodes.length ===
0 and true after successfully updating the first t node and clearing the rest,
and then update callers such as replace_paragraph_at_body_index to check the
boolean and set matched: 0 (or status accordingly) when false; alternatively you
may choose to throw an error instead of returning false, but ensure
replace_paragraph_at_body_index is updated to handle the new behavior.
- Around line 142-181: getTableContent currently always treats the first row as
headers which mislabels data tables; change getTableContent to either accept an
optional flag (e.g., treatFirstRowAsHeader?: boolean) or implement a quick
bold-detection heuristic before promoting the first row to headers: add a helper
like hasBoldInCell(cell: Element): boolean that inspects cell child runs (w:r ->
w:rPr -> w:b or w:strong) and use it on every cell of the first row (or require
the flag) so headers is only set when the flag is true or at least one/most
first-row cells have bold styling; update the function signature and calls
accordingly (reference getTableContent and getCellText to locate where to add
the flag/helper).

In `@src/tools/docx/ops/index.ts`:
- Around line 32-70: The dispatcher applyOp is synchronous and blocks the event
loop because applyInsertImage uses sync FS calls; change applyOp's signature to
async (return Promise<OpResult>) and convert the switch to await async handlers;
make applyInsertImage async and replace fs.readFileSync/fs.existsSync with
fs.promises APIs (or other non-blocking reads) and propagate async changes back
to callers (e.g., the loop in write.ts should await applyOp). Update any other
ops you change to return Promise<OpResult> so the switch can uniformly await
each handler.

In `@src/tools/docx/read.ts`:
- Around line 6-8: This file currently constructs new DOMParser/XMLSerializer
instances and calls new DOMParser().parseFromString(...) in several places
(mentions of DOMParser, XMLSerializer); replace those usages with the shared
utilities parseXml and serializeXml (the exported functions from dom.ts) so
parsing/serialization goes through the centralized helpers; update all
occurrences (the parseFromString call sites and any direct XMLSerializer usage)
to call parseXml(...) and serializeXml(...) respectively, remove the direct
imports of DOMParser/XMLSerializer, and ensure any code depending on the return
types still works with the DOM Document/Node returned by parseXml.
- Around line 328-333: The slice logic for paragraphs mishandles negative
options.offset (options.offset || 0 then slice(offset, offset + length)) causing
empty results; update the computation to support negative-offset tail behavior:
compute a normalized start index like const start = options.offset !== undefined
? (options.offset >= 0 ? options.offset : Math.max(allParagraphs.length +
options.offset, 0)) : 0, compute length as before (const length = options.length
!== undefined ? options.length : allParagraphs.length), then set paragraphs =
allParagraphs.slice(start, Math.min(start + length, allParagraphs.length));
adjust references to options.offset, options.length, allParagraphs and
paragraphs accordingly so negative offsets return the tail segment.
- Around line 271-280: getDocxMetadata currently reloads the same file by
calling loadDocx(path) and extractParagraphs(path), causing redundant reads;
change getDocxMetadata to accept an already-loaded zip (returned from loadDocx)
and the parsed paragraphs (from extractParagraphs) instead of a path, update its
signature and internal logic to use those inputs, and modify readDocx to call
getDocxMetadata(zip, paragraphs) after it has loaded the zip and parsed
paragraphs; to preserve compatibility you can add an overload or optional
parameters so callers that still pass a path fall back to the existing behavior,
and update any references to loadDocx/extractParagraphs inside getDocxMetadata
accordingly.

In `@src/tools/schemas.ts`:
- Around line 190-198: InsertImageOpSchema and DocxContentImageSchema allow
zero/negative image dimensions because width and height are declared as
z.number().optional(); update both schemas so the width and height validators
include .min(1) (e.g., z.number().min(1).optional()) to enforce positive
dimensions and prevent invalid DOCX drawing XML; modify the width and height
entries in InsertImageOpSchema and the corresponding fields in
DocxContentImageSchema accordingly.
🧹 Nitpick comments (3)
src/tools/docx/ops/set-color-for-paragraph-exact.ts (1)

21-30: Inner loop variable i shadows outer loop variable i.

Line 26 declares let i which shadows the outer let i on Line 21. While let is block-scoped so this works correctly at runtime, it's a readability hazard. Additionally, the inner loop reimplements what colorParagraphRuns(p, op.color) from dom.ts already does.

Proposed fix
-        const runs = p.getElementsByTagName('w:r');
-        for (let i = 0; i < runs.length; i++) {
-            ensureRunColor(runs.item(i) as Element, op.color);
-        }
+        colorParagraphRuns(p, op.color);

With import update:

-import { getParagraphText, ensureRunColor } from '../dom.js';
+import { getParagraphText, colorParagraphRuns } from '../dom.js';
src/server.ts (2)

25-34: DC_PRIORITY_NOTE is injected only into the get_config tool description.

This routing instruction is critical for correct tool selection, but it's embedded in get_config's description (Line 227), which is semantically unrelated to file routing. If a client doesn't call get_config early in a session, the LLM may never see this guidance.

Consider whether this should also appear in read_file or as a top-level server instruction/system prompt, depending on how the MCP client surfaces tool descriptions.


497-608: write_docx tool description is comprehensive but very long.

The description is ~110 lines with detailed operation examples. While thorough for LLM guidance, this consumes significant tokens in every list_tools response. Consider whether some of this detail (especially per-op examples on Lines 542-588) could be shortened or moved to a linked reference, keeping only the workflow and key constraints inline.

That said, this is a tradeoff — more detail helps the LLM use the tool correctly.

Comment on lines +142 to +181
export function getTableContent(tbl: Element): { headers?: string[]; rows: string[][] } {
const rows: Element[] = [];
for (const child of nodeListToArray(tbl.childNodes)) {
if (child.nodeType === 1 && (child as Element).nodeName === 'w:tr') {
rows.push(child as Element);
}
}

if (rows.length === 0) {
return { rows: [] };
}

// Extract cells from each row
const tableRows: string[][] = [];
for (const row of rows) {
const cells: string[] = [];
for (const child of nodeListToArray(row.childNodes)) {
if (child.nodeType === 1 && (child as Element).nodeName === 'w:tc') {
cells.push(getCellText(child as Element));
}
}
if (cells.length > 0) {
tableRows.push(cells);
}
}

// First row might be header - check if it has bold formatting
// For simplicity, we'll treat first row as potential header
// User can determine this based on style or content
if (tableRows.length > 0) {
const firstRow = tableRows[0];
const restRows = tableRows.slice(1);
return {
headers: firstRow.length > 0 ? firstRow : undefined,
rows: restRows.length > 0 ? restRows : [],
};
}

return { rows: tableRows };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getTableContent unconditionally treats the first row as headers.

Every table's first row is split out as headers, even when the table has no actual header row. This produces misleading outlines for data-only tables. Since this feeds into ReadDocxResult.tables[].headers, downstream consumers (and LLM callers) will assume bold/header semantics.

Consider making this heuristic opt-in or checking for bold formatting on the first row's cells (the comment on Line 168 already hints at this).

🤖 Prompt for AI Agents
In `@src/tools/docx/dom.ts` around lines 142 - 181, getTableContent currently
always treats the first row as headers which mislabels data tables; change
getTableContent to either accept an optional flag (e.g., treatFirstRowAsHeader?:
boolean) or implement a quick bold-detection heuristic before promoting the
first row to headers: add a helper like hasBoldInCell(cell: Element): boolean
that inspects cell child runs (w:r -> w:rPr -> w:b or w:strong) and use it on
every cell of the first row (or require the flag) so headers is only set when
the flag is true or at least one/most first-row cells have bold styling; update
the function signature and calls accordingly (reference getTableContent and
getCellText to locate where to add the flag/helper).

Comment on lines +227 to +238
export function setParagraphTextMinimal(p: Element, text: string): void {
const tNodes = p.getElementsByTagName('w:t');
if (tNodes.length === 0) return;

const first = tNodes.item(0)!;
first.textContent = text;
first.setAttribute('xml:space', 'preserve');

for (let i = 1; i < tNodes.length; i++) {
tNodes.item(i)!.textContent = '';
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

setParagraphTextMinimal silently no-ops when the paragraph has no <w:t> nodes.

If a paragraph contains no runs/text nodes (e.g., an empty paragraph or one with only drawing elements), this function returns without modifying anything and without signaling to callers. Operations like replace_paragraph_at_body_index that rely on this will report status: 'applied' even though no text was actually set.

Consider returning a boolean or throwing so callers can set matched: 0 appropriately.

🤖 Prompt for AI Agents
In `@src/tools/docx/dom.ts` around lines 227 - 238, setParagraphTextMinimal
currently silently no-ops when there are no <w:t> nodes; change its signature to
return a boolean (true if text was actually set, false otherwise), have it
return false when tNodes.length === 0 and true after successfully updating the
first t node and clearing the rest, and then update callers such as
replace_paragraph_at_body_index to check the boolean and set matched: 0 (or
status accordingly) when false; alternatively you may choose to throw an error
instead of returning false, but ensure replace_paragraph_at_body_index is
updated to handle the new behavior.

Comment on lines +32 to +70
export function applyOp(body: Element, op: DocxOp, zip?: PizZip): OpResult {
switch (op.type) {
case 'replace_paragraph_text_exact':
return applyReplaceParagraphTextExact(body, op);
case 'replace_paragraph_at_body_index':
return applyReplaceParagraphAtBodyIndex(body, op);
case 'set_color_for_style':
return applySetColorForStyle(body, op);
case 'set_color_for_paragraph_exact':
return applySetColorForParagraphExact(body, op);
case 'set_paragraph_style_at_body_index':
return applySetParagraphStyleAtBodyIndex(body, op);
case 'insert_paragraph_after_text':
return applyInsertParagraphAfterText(body, op);
case 'delete_paragraph_at_body_index':
return applyDeleteParagraphAtBodyIndex(body, op);
case 'table_set_cell_text':
return applyTableSetCellText(body, op);
case 'replace_table_cell_text':
return applyReplaceTableCellText(body, op);
case 'replace_hyperlink_url':
if (!zip) return { op, status: 'skipped', matched: 0, reason: 'zip_required_for_hyperlink_op' };
return applyReplaceHyperlinkUrl(body, op, zip);
case 'header_replace_text_exact':
if (!zip) return { op, status: 'skipped', matched: 0, reason: 'zip_required_for_header_op' };
return applyHeaderReplaceTextExact(body, op, zip);
case 'insert_table':
return applyInsertTable(body, op);
case 'insert_image':
if (!zip) return { op, status: 'skipped', matched: 0, reason: 'zip_required_for_image_op' };
return applyInsertImage(body, op, zip);
default:
return {
op,
status: 'skipped',
matched: 0,
reason: `unknown_op_type: ${(op as any).type}`,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Synchronous dispatcher will block the event loop for insert_image ops.

applyOp is synchronous (→ OpResult), but applyInsertImage (per PR review blocker #4) uses fs.readFileSync / fs.existsSync internally to read image files from disk. This blocks the Node.js event loop during image insertion.

Make applyOp async (returning Promise<OpResult>) and convert image ops to use fs.promises. Since the caller in write.ts likely iterates ops in a loop, switching to async/await here is straightforward.

Proposed signature change
-export function applyOp(body: Element, op: DocxOp, zip?: PizZip): OpResult {
+export async function applyOp(body: Element, op: DocxOp, zip?: PizZip): Promise<OpResult> {

Then convert applyInsertImage to async as well, and await it at the call site (Line 62).

🤖 Prompt for AI Agents
In `@src/tools/docx/ops/index.ts` around lines 32 - 70, The dispatcher applyOp is
synchronous and blocks the event loop because applyInsertImage uses sync FS
calls; change applyOp's signature to async (return Promise<OpResult>) and
convert the switch to await async handlers; make applyInsertImage async and
replace fs.readFileSync/fs.existsSync with fs.promises APIs (or other
non-blocking reads) and propagate async changes back to callers (e.g., the loop
in write.ts should await applyOp). Update any other ops you change to return
Promise<OpResult> so the switch can uniformly await each handler.

Comment on lines +6 to +8
import fs from 'fs/promises';
import PizZip from 'pizzip';
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Inconsistent XML parsing — bypasses shared parseXml utility from dom.ts.

The file imports DOMParser and XMLSerializer directly from @xmldom/xmldom (Line 8) and calls new DOMParser().parseFromString(...) in multiple places (Lines 53, 110, 208, 230, 255, 290, 312) instead of using the shared parseXml/serializeXml from dom.ts.

This means any future error-handling improvements to parseXml (already flagged in a prior review) won't apply here. Consolidate to use the shared utilities.

Also applies to: 53-53, 110-110, 208-208, 230-230, 255-255, 290-290, 312-312

🤖 Prompt for AI Agents
In `@src/tools/docx/read.ts` around lines 6 - 8, This file currently constructs
new DOMParser/XMLSerializer instances and calls new
DOMParser().parseFromString(...) in several places (mentions of DOMParser,
XMLSerializer); replace those usages with the shared utilities parseXml and
serializeXml (the exported functions from dom.ts) so parsing/serialization goes
through the centralized helpers; update all occurrences (the parseFromString
call sites and any direct XMLSerializer usage) to call parseXml(...) and
serializeXml(...) respectively, remove the direct imports of
DOMParser/XMLSerializer, and ensure any code depending on the return types still
works with the DOM Document/Node returned by parseXml.

Comment on lines +271 to +280
/** Get comprehensive metadata. */
export async function getDocxMetadata(path: string): Promise<DocxMetadata> {
const zip = await loadDocx(path);
const paragraphs = await extractParagraphs(path);
const coreProps = await getCoreProperties(zip);
const fullText = paragraphs.map((p) => p.text).join(' ');
const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;

return { ...coreProps, paragraphCount: paragraphs.length, wordCount };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getDocxMetadata re-reads the file and re-parses paragraphs redundantly.

getDocxMetadata calls loadDocx(path) (Line 273) and extractParagraphs(path) (Line 274), which internally calls loadDocx(path) again. When called from readDocx (Line 335), the same file has already been loaded on Line 307 — totaling 3 reads of the same file.

Refactor to accept the already-loaded zip and parsed paragraphs:

Proposed fix
-export async function getDocxMetadata(path: string): Promise<DocxMetadata> {
-    const zip = await loadDocx(path);
-    const paragraphs = await extractParagraphs(path);
+export async function getDocxMetadata(
+    path: string,
+    zip?: PizZip,
+    paragraphs?: DocxParagraph[],
+): Promise<DocxMetadata> {
+    zip = zip ?? await loadDocx(path);
+    paragraphs = paragraphs ?? await extractParagraphs(path);
     const coreProps = await getCoreProperties(zip);
     const fullText = paragraphs.map((p) => p.text).join(' ');
     const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
 
     return { ...coreProps, paragraphCount: paragraphs.length, wordCount };
 }
🤖 Prompt for AI Agents
In `@src/tools/docx/read.ts` around lines 271 - 280, getDocxMetadata currently
reloads the same file by calling loadDocx(path) and extractParagraphs(path),
causing redundant reads; change getDocxMetadata to accept an already-loaded zip
(returned from loadDocx) and the parsed paragraphs (from extractParagraphs)
instead of a path, update its signature and internal logic to use those inputs,
and modify readDocx to call getDocxMetadata(zip, paragraphs) after it has loaded
the zip and parsed paragraphs; to preserve compatibility you can add an overload
or optional parameters so callers that still pass a path fall back to the
existing behavior, and update any references to loadDocx/extractParagraphs
inside getDocxMetadata accordingly.

Comment on lines +328 to +333
let paragraphs = allParagraphs;
if (options?.offset !== undefined || options?.length !== undefined) {
const offset = options.offset || 0;
const length = options.length !== undefined ? options.length : allParagraphs.length;
paragraphs = allParagraphs.slice(offset, offset + length);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Negative offset values produce incorrect/empty results.

options.offset || 0 is falsy-coalescing (fine), but allParagraphs.slice(offset, offset + length) with a negative offset breaks: e.g., slice(-5, -5 + 10)slice(-5, 5) — on a 20-element array this is slice(15, 5) which returns [].

The read_file tool description advertises negative-offset tail behavior for DOCX, but this implementation doesn't support it. Either implement tail behavior (as done for text files) or document the limitation.

Proposed fix for negative offset support
     let paragraphs = allParagraphs;
     if (options?.offset !== undefined || options?.length !== undefined) {
-        const offset = options.offset || 0;
-        const length = options.length !== undefined ? options.length : allParagraphs.length;
-        paragraphs = allParagraphs.slice(offset, offset + length);
+        const offset = options.offset ?? 0;
+        const length = options.length ?? allParagraphs.length;
+        if (offset < 0) {
+            paragraphs = allParagraphs.slice(offset); // tail behavior
+        } else {
+            paragraphs = allParagraphs.slice(offset, offset + length);
+        }
     }
📝 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
let paragraphs = allParagraphs;
if (options?.offset !== undefined || options?.length !== undefined) {
const offset = options.offset || 0;
const length = options.length !== undefined ? options.length : allParagraphs.length;
paragraphs = allParagraphs.slice(offset, offset + length);
}
let paragraphs = allParagraphs;
if (options?.offset !== undefined || options?.length !== undefined) {
const offset = options.offset ?? 0;
const length = options.length ?? allParagraphs.length;
if (offset < 0) {
paragraphs = allParagraphs.slice(offset); // tail behavior
} else {
paragraphs = allParagraphs.slice(offset, offset + length);
}
}
🤖 Prompt for AI Agents
In `@src/tools/docx/read.ts` around lines 328 - 333, The slice logic for
paragraphs mishandles negative options.offset (options.offset || 0 then
slice(offset, offset + length)) causing empty results; update the computation to
support negative-offset tail behavior: compute a normalized start index like
const start = options.offset !== undefined ? (options.offset >= 0 ?
options.offset : Math.max(allParagraphs.length + options.offset, 0)) : 0,
compute length as before (const length = options.length !== undefined ?
options.length : allParagraphs.length), then set paragraphs =
allParagraphs.slice(start, Math.min(start + length, allParagraphs.length));
adjust references to options.offset, options.length, allParagraphs and
paragraphs accordingly so negative offsets return the tail segment.

Comment on lines +190 to +198
const InsertImageOpSchema = z.object({
type: z.literal('insert_image'),
after: z.string().optional(),
before: z.string().optional(),
imagePath: z.string(),
width: z.number().optional(),
height: z.number().optional(),
altText: z.string().optional(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Image width/height lack minimum value constraints.

z.number().optional() allows zero or negative dimensions, which would produce invalid drawing XML in the DOCX. Add .min(1) to prevent nonsensical image sizes.

Proposed fix (apply to both InsertImageOpSchema and DocxContentImageSchema)
-  width: z.number().optional(),
-  height: z.number().optional(),
+  width: z.number().min(1).optional(),
+  height: z.number().min(1).optional(),

Also applies to: 230-236

🤖 Prompt for AI Agents
In `@src/tools/schemas.ts` around lines 190 - 198, InsertImageOpSchema and
DocxContentImageSchema allow zero/negative image dimensions because width and
height are declared as z.number().optional(); update both schemas so the width
and height validators include .min(1) (e.g., z.number().min(1).optional()) to
enforce positive dimensions and prevent invalid DOCX drawing XML; modify the
width and height entries in InsertImageOpSchema and the corresponding fields in
DocxContentImageSchema accordingly.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 13, 2026

CodeAnt AI is running Incremental review


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

@codeant-ai codeant-ai bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Feb 13, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 13, 2026

CodeAnt AI Incremental review completed.

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