{"slug": "show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens", "title": "Show HN: Docx-CLI: agents read/edit Word docs using 1/2 the time and tokens", "summary": "Docx-CLI, a command-line tool for AI agents to edit Word documents without breaking formatting, achieves up to 6x better task completion and uses 2.2-2.6x fewer tokens compared to the default method of hand-writing OOXML, according to a controlled bake-off with six real document tasks.", "body_md": "**A .docx CLI built for AI agents.** Leave comments, suggest redlines, and edit Word documents without breaking the formatting or losing content — a human accepts or rejects in Word afterward.\n\n- Hand a\n`.docx`\n\nto Claude or Codex and get back a redlined copy with comments — open it in Word, accept or reject as usual. - Agents address text by\n**stable locators** with character offsets (`p3:5-20`\n\n); humans see normal Word formatting on disk. - Custom styles, theme colors, embedded objects — all of it survives. The CLI mutates XML in place rather than re-emitting from a lossy model.\n\nThe default way agents edit Word docs is to unzip the `.docx`\n\nand hand-write the OOXML inside. That takes a strong model to get right, burns tokens, and routinely produces a file Word won't open. `docx-cli`\n\nhands the agent plain commands plus an annotated-Markdown read view, so it never has to reason about the XML.\n\nWe measured it — a controlled A/B bake-off: **six real document tasks** (fill an NDA, fill an invoice, restyle a résumé, redline a contract, finalize a contract, author a journal), the same starting files, and one independent judge grading every result from the **Word-rendered pages**. Three runs per arm at each of two model tiers:\n\n| Haiku (weak, cheap) | Sonnet (strong) | |||\n|---|---|---|---|---|\ndocx-cli |\ndefault skill | docx-cli |\ndefault skill | |\n| Tasks solved (of 6) | 4.3 (4–5) |\n0.7 (0–1) | 6.0 (6–6) |\n4.0 (4–4) |\n| Rendered correctly (of 6) | 5.7 | 3.7 | 6.0 | 4.7 |\n| Outright-broken documents | 0 |\n~1/run (up to 2) | 0 |\n0 |\n| Input tokens | 2.4M |\n6.1M (2.6×) | 1.6M |\n3.6M (2.2×) |\n| Wall-clock | 924 s |\n1,882 s (2.0× slower) | 1,175 s |\n2,029 s (1.7× slower) |\n\n**The correctness gap is widest on the cheap Haiku tier (~6×)**, and a frontier model never closes it — the default skill caps at 4/6, losing the contract redline and the résumé every Sonnet run.**The cost and speed penalties are model-independent**— ~2.2–2.6× more tokens and ~1.7–2× slower at*both*tiers, with token/time ranges that never overlap.**Word couldn't reliably open the default skill's work**— it failed to open 5 of 36 of its outputs; all 36 of docx-cli's opened on the first try.\n\nFull methodology, per-task rubric, and side-by-side renders: ** the bake-off writeup**.\n\n**npm** — the simplest path (requires Bun >= 1.3):\n\n```\nbun add -g bun-docx\n# or run without installing:\nbunx bun-docx read doc.docx\n```\n\n**Standalone binary** (no Bun required). Every release publishes prebuilt binaries plus a `SHA256SUMS`\n\nmanifest, and the installer **verifies the binary's SHA-256** before installing:\n\n```\ncurl -fsSL https://raw.githubusercontent.com/kklimuk/docx-cli/main/install.sh | sh\n```\n\nHonors `PREFIX`\n\n(default `$HOME/.local/bin`\n\n) and `VERSION`\n\n(default `latest`\n\n). Pre-built for linux/x64, linux/arm64, darwin/x64, darwin/arm64, windows/x64. Prefer to inspect first? Download `docx-<platform>`\n\n+ `SHA256SUMS`\n\nfrom the [latest release](https://github.com/kklimuk/docx-cli/releases/latest), verify, `chmod +x`\n\n, and put it on `PATH`\n\n.\n\nThe repo includes a Common Paper Mutual NDA template at `tests/fixtures/mnda.docx`\n\n. Below are the primitives an agent would compose to fill in the cover page and leave redline edits — the same flow shown in the video above. Every command was verified end-to-end against the fixture:\n\n```\n# Make a copy first — there's no undo (git is the history; the CLI overwrites in place)\ncp tests/fixtures/mnda.docx mnda-filled.docx\n\n# Read the cover-page table so the agent knows what placeholders exist\ndocx read mnda-filled.docx --from t1 --to t1\n\n# Fill the yellow-highlighted bracketed placeholders\ndocx replace mnda-filled.docx \"Fill in: today's date\" \"May 6, 2026\"\ndocx replace mnda-filled.docx \"fill in state and/or county\" \"California\"\ndocx replace mnda-filled.docx \"fill in state\" \"California\"\ndocx replace mnda-filled.docx \"Fill in, if any.\" \"None.\"\n\n# Verify nothing's left to fill (bare locator lines, one per match; nothing → exit 0)\ndocx find mnda-filled.docx '\\[(Fill|fill)[^]]*\\]' --regex --all\n\n# Flip on tracked changes for the redline pass\ndocx track-changes mnda-filled.docx on\n\n# Tighten \"having a reasonable need to know\" in the Use & Protection clause\ndocx replace mnda-filled.docx \\\n    \"having a reasonable need to know\" \\\n    \"with a documented need to know\"\n\n# Leave a comment for the human reviewer — addresses an existing span with --at\ndocx comments add mnda-filled.docx --at p7:0-30 \\\n    --text \"Should we narrow 'representatives' to a named list?\"\n```\n\nOpen `mnda-filled.docx`\n\nin Word: tracked changes and comments appear in the review pane, ready to accept, reject, or reply. Or run `docx track-changes accept mnda-filled.docx --all`\n\nto bake them in from the CLI.\n\ndocx-cli ships as an [Agent Skill](https://agentskills.io) — one `SKILL.md`\n\nthat works across Claude Code, Codex, Pi, and the other harnesses that read the open skill format. The skill teaches the locator model and the redline / comment / fill workflows, then defers to `docx <command> --help`\n\nat runtime, so it can't go stale.\n\n**Why a skill?** docx-cli is built for the *weakest, cheapest* agents. In our weak-agent benchmark — 6 real document tasks (fill a contract, redline, comment, restyle, author from scratch), graded against Word renders, 3 runs each — Haiku driving docx-cli completed **4.3/6** tasks versus **0.7/6** for the default Claude skill, at roughly **2.5× fewer tokens**; with Sonnet it's **6/6 vs 4/6**, with roughly 2x fewer tokens. And every docx-cli output opened cleanly in Word on the first try — it never emits a file the renderer rejects. (Methodology and harness: [ .claude/skills/weak-agent-test](/kklimuk/docx-cli/blob/main/.claude/skills/weak-agent-test).)\n\n**Any agent ( skills.sh)** — one cross-harness command, installs into whichever agent you're using:\n\n```\nnpx skills add kklimuk/docx-cli\n```\n\n**Claude Code** — one-line plugin install:\n\n```\n/plugin marketplace add kklimuk/docx-cli\n/plugin install docx-cli@docx-cli\n```\n\n**Codex** — add the marketplace (the plugin's skills auto-discover):\n\n```\ncodex plugin marketplace add kklimuk/docx-cli\n```\n\n**Pi** — one-command install (the `pi`\n\nmanifest in `package.json`\n\npulls in the skill), then invoke `/skill:docx-cli`\n\n:\n\n```\npi install git:github.com/kklimuk/docx-cli      # global; add -l for a project (team-shared) install\n# manual alternative: pi --skill /path/to/docx-cli/skills/docx-cli\n```\n\n**Any harness / manual** — drop [ skills/docx-cli/](/kklimuk/docx-cli/blob/main/skills/docx-cli) into your agent's skills directory (e.g.\n\n`~/.claude/skills/`\n\nor the cross-tool `~/.agents/skills/`\n\n). On first activation the skill's [installs the](/kklimuk/docx-cli/blob/main/skills/docx-cli/scripts/bootstrap.sh)\n\n`scripts/bootstrap.sh`\n\n`docx`\n\nbinary (and self-updates a stale one).The binary is the source of truth: `docx info skill`\n\nprints the canonical `SKILL.md`\n\nfor the installed version, and a CI test fails if the committed copy drifts. Regenerate after any change with:\n\n```\ndocx info skill > skills/docx-cli/SKILL.md\n```\n\nAgents: runEvery command's`docx <command> --help`\n\nbefore composing a call.`--help`\n\nis the source of truth for its flags, locator forms, and exact output shape — this README is a map, not the territory. Two more must-reads:\n\n— the canonical locator grammar (`docx info locators`\n\n`--json`\n\nfor a machine-readable form). The top-level`docx --help`\n\nsays it outright:\"It is highly recommended to agents to run`docx info locators`\n\nto understand their capabilities.\"— the AST type definitions (`docx info schema`\n\n`--ts`\n\nfor TypeScript source) that`read --ast`\n\nemits.\n\n`docx <verb>`\n\nand `docx <noun> <verb>`\n\n. Every command has `--help`\n\n. Two groups: **read/query** commands print data to stdout; **mutate** commands change the file (and accept `--dry-run`\n\n, `-o/--output PATH`\n\n, `-v/--verbose`\n\n).\n\n```\ndocx read    FILE [--from LOC] [--to LOC] [--accepted | --baseline | --current] [--comments]\ndocx read    FILE --ast                  # JSON-AST instead of Markdown (disables the markdown-only flags)\ndocx find    FILE QUERY [--regex] [--ignore-case] [--all] [--nth N] [--current | --baseline] [--exact] [--json]\ndocx find    FILE (--highlight COLOR|any | --color HEX | --bold | --italic | --underline) [--all] [--json]   # find by formatting (no QUERY)\ndocx wc      FILE [LOCATOR] [--accepted | --baseline | --current] [--json]\ndocx outline FILE [--style-prefix S] [--json]\ndocx styles  FILE [--used] [--at STYLEID] [--json]   # the style catalog (not in the body) — what --style NAMEs exist\ndocx styles  --catalog [--json]                      # built-in styles you can apply on demand (Title, Heading1–9, Quote, …), no FILE needed\ndocx styles  set    FILE --at STYLEID [--bold --color HEX --size PT --font NAME --space-before PT --indent-left IN …]   # restyle every paragraph/run that uses the style\ndocx styles  create FILE STYLEID [--type paragraph|character] [--name \"…\"] [--based-on STYLEID] [--next STYLEID] [formatting]   # define a new custom style\ndocx render  FILE [--out DIR] [--engine word|libreoffice|auto] [--dpi N] [--pages 1-N] [--format png|jpg]\n\ndocx comments      list FILE [--include-resolved] [--thread cN]\ndocx footnotes     list FILE\ndocx endnotes      list FILE\ndocx headers       list FILE\ndocx footers       list FILE\ndocx images        list FILE\ndocx hyperlinks    list FILE\ndocx track-changes list FILE\n\ndocx info schema   [--ts]\ndocx info locators [--json]\n```\n\n`docx read`\n\nsurfaces structural facts the Markdown body can't show as HTML-comment\nannotations (`<!-- docx:TYPE … -->`\n\n). These are **read-time visibility hints** — the\nagent can SEE the structure, but the importer drops them (the structure survives\nnormal edits in place, `read --ast`\n\nis the lossless view, and `docx sections`\n\n/\n`docx tables …`\n\nmanage it). They're emitted **deviation-only**\n(only when a value differs from the document default, so a plain document stays\nclean):\n\n**Per-paragraph style/spacing/indent**— the most common annotation — rides a`<!-- docx:p pN style=\"Caption\" align=\"center\" space-after=\"6pt\" line-spacing=\"1\" indent-left=\"0.25in\" -->`\n\nnote, emitted deviation-only (only the attrs that differ from the style/document default). Each attribute maps to the matching`edit`\n\n/`insert`\n\nflag (`--style`\n\n,`--alignment`\n\n,`--space-before`\n\n/`--space-after`\n\n,`--line-spacing`\n\n,`--indent-left`\n\n/`--indent-right`\n\n/`--first-line`\n\n/`--hanging`\n\n), so an agent reads a value and re-applies it. The paragraph's locator rides this note as its leading`pN`\n\ntoken, so an annotated paragraph does NOT also get a bare`<!-- pN -->`\n\n(only undeviating paragraphs get the bare locator). Full properties are in`read --ast`\n\n.**Section breaks** render as`<!-- docx:section sN cols=\"2\" type=\"continuous\" -->`\n\non their own line — never a bare`---`\n\n(that's a thematic break, and emitting it for a section silently turned layout into border paragraphs). A hand-authored`---`\n\nnow unambiguously means a thematic break.**Page geometry** rides a leading`<!-- docx:page sN orientation=\"landscape\" size=\"…in\" margins=\"…in\" text-width=\"…in\" -->`\n\nnote when the page deviates from US-Letter-portrait-1″ —`text-width`\n\nis the usable column width, and the leading`sN`\n\nis the section to re-apply against. A`varies=\"by-section\"`\n\nattribute is added when a later section's page setup differs from the leading one — and in that case the note fires**even if page 1 is plain default Letter-portrait-1″**(it then shows just`text-width`\n\n+`varies=\"by-section\"`\n\n), warning that the geometry shown describes only the leading section; use`read --ast`\n\nfor every section's exact geometry. Exact twips are in`read --ast`\n\n(on each section break:`pageWidth`\n\n/`pageHeight`\n\n/`pageOrientation`\n\n/`margin*`\n\n). Set it for the WHOLE document with`docx sections --orientation/--size/--margins`\n\n(no`--at`\n\n→ every section gets it, so a multi-section doc doesn't leave the trailing section behind), one section with`docx sections --at sN …`\n\n, or at`create`\n\ntime; under track-changes it records as one`<w:sectPrChange>`\n\nper section (accept/reject in Word). Changing margins/size also**auto-realigns right-edge tab columns**(résumé dates/locations): a LEFT tab calibrated to the old margins would overflow and wrap at the new width, so page setup converts each to a RIGHT tab flush at the new margin and reports how many it fixed — no second`--tabs right`\n\nstep needed.**Tables** carry a leading`<!-- docx:table t0 widths=\"1,2,3in\" borders=\"double\" -->`\n\nwhen columns are uneven or borders deviate from the default, plus a per-cell`<!-- docx:cell t0:r0c0 gridSpan=\"2\" vMerge=\"continue\" shading=\"FFE699\" -->`\n\nnote on merged/shaded cells — so structure invisible in GFM is visible (`Table.borders`\n\n/`TableCell.shading`\n\nin`read --ast`\n\n).**Images** trail a`<!-- docx:image img0 size=\"6.2x4.1in\" float=\"yes\" wrap=\"square\" align=\"center\" overflow=\"yes\" -->`\n\nnote:`size`\n\nalways (the`![](hash)`\n\nalone doesn't say \"6in wide\"), and`float`\n\n/`wrap`\n\n/`align`\n\n/`overflow`\n\nonly when they deviate (an inline, in-bounds image shows just its size).`overflow`\n\nflags an image wider than the usable text column (`ImageRun.floating`\n\n/`wrap`\n\n/`align`\n\n+ EMU extents in`read --ast`\n\n).**Headers / footers** surface as`<!-- docx:header text=\"Quarterly Report\" -->`\n\n/`<!-- docx:footer text=\"Page {page} of {pages}\" -->`\n\nnotes (the`type`\n\nattr appears only for`first`\n\n/`even`\n\n). Fields read as tokens —`{page}`\n\n`{pages}`\n\n`{date}`\n\n`{time}`\n\n`{styleref:NAME}`\n\n`{filename}`\n\n`{title}`\n\n`{author}`\n\n(`{time}`\n\nread-only). A marginal that's the same on every section rides the top; one that**differs by section** renders at that section's**start**(alongside the`docx:section`\n\nnote, which also renders at the section's start with`applies-to=\"… (below)\"`\n\n), so each hint reads right before the content it governs. The text lives in the comment attribute so the importer drops it (it can't re-inject into the body); full entries are in`read --ast`\n\nunder`headers`\n\n/`footers`\n\n(`Marginal[]`\n\n). Set with`docx headers`\n\n/`docx footers`\n\n.**Track-changes state** rides a head`<!-- docx:track-changes on -->`\n\nline when the document's tracking toggle is enabled (deviation-only — off emits nothing), so an agent sees that subsequent edits will be redlined without inspecting`settings.xml`\n\n. Toggle it with`docx track-changes FILE on|off`\n\n; the three tracked-change read views (`--accepted`\n\n/`--current`\n\n/`--baseline`\n\n) are covered under the review loop below.\n\n### Mutate (change FILE in place; `--dry-run`\n\n, `-v`\n\neverywhere; `-o PATH`\n\non every mutator except `create`\n\n, whose positional FILE is already the output)\n\n```\ndocx create FILE [--title T] [--author A] [--text \"...\" | --text-file PATH | --from PATH.md | --from -] [--orientation O] [--size SIZE] [--margins M] [--header \"...\"] [--footer \"...\" | --page-numbers] [--force]\ndocx insert FILE (--after | --before) LOCATOR <content>   # LOCATOR = pN | tN | sN | tN:rRcC:pK\ndocx insert FILE (--at-start | --at-end) <content>        # no locator — prepend / append to the document\ndocx edit   FILE --at LOCATOR <content>                   # LOCATOR = pN | pN:S-E | pN-pM | sN | eqN | tN:rRcC:pK[:S-E]\ndocx delete FILE --at LOCATOR                             # LOCATOR = pN | pN-pM | tN | sN | tN:rRcC:pK (cell paragraph)\ndocx sections FILE [--at LOCATOR] [--columns N] [--type T] [--orientation O] [--size SIZE] [--margins M]   # LOCATOR = pN-pM | pN (wrap a range in N columns) | sN (edit one section's columns/type/page geometry). Multi-column layout AND page setup live HERE. PAGE GEOMETRY (margins/orientation/size) with NO --at applies to the WHOLE document (every section); --at sN targets one. Columns/type need --at.\ndocx styles set-default-font FILE \"Font Name\" [--size N] [--all]   # document-wide font: sets styles.xml docDefaults + theme major/minor; --all also repoints styles/runs that pin their own font\ndocx replace FILE PATTERN REPLACEMENT [--at pN] [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]\n#   Keeps the run's formatting (bold/font) and any tabs — the no-rebuild way to fill a\n#   formatted/tabbed template line (e.g. \"**Org Name**⇥Date\"); don't hand-build --runs to refill it.\n#   --at pN (or a cell paragraph tT:rRcC:pN) CONFINES the replace to one paragraph — use it when the\n#   SAME placeholder repeats across the doc (a résumé's \"City, State\" in every entry) and you want THE\n#   one in a specific paragraph, instead of find → edit --at pN:S-E span surgery. Batch entries take \"at\" too.\n\n# Batch — apply many changes from ONE read (no re-reading between edits). Keys\n# on each JSONL line mirror the command's flags; all locators address the doc as\n# read. insert/edit also accept --batch - to read JSONL from stdin.\ndocx edit    FILE --batch fills.jsonl       # { at, <one of: text|clear|markdown|runs|code|task>, style?, … }\ndocx insert  FILE --batch additions.jsonl   # { after|before, <content>, style?, color?, … }\ndocx replace FILE --batch script.jsonl      # { pattern, replacement, at?, regex?, all?, limit?, … } applied in order (\"at\" scopes that entry to one paragraph)\ndocx delete  FILE --batch drop.jsonl        # { at } per line — whole blocks (pN/tN/cell), resolved live-first\n\n# All four of insert/edit/delete/replace accept --track to record that one\n# invocation as a tracked change even when the doc's track-changes toggle is off.\n#\n# insert/edit content selectors (run \"docx insert --help\" / \"docx edit --help\" for the full list):\n#   --text \"...\" [--style NAME] [--alignment A] [--color HEX] [--bold] [--italic] [--url URL]\n#       (a newline in --text becomes a line break <w:br/>, a tab becomes <w:tab/> — verse/addresses stay line-per-line)\n#   paragraph spacing/indent (insert + edit, alone or with content, per-entry in --batch, across a range):\n#       --space-before PT --space-after PT --line-spacing N(=1|1.5|2|single|double, or 15pt / \"15pt atLeast\")\n#       --indent-left IN --indent-right IN --first-line IN --hanging IN  (points / inches; first-line ⊥ hanging;\n#       left/right/first-line accept a negative value to outdent into the margin; hanging stays non-negative)\n#       Under track-changes these record a tracked <w:pPrChange> (accept/reject in Word) — even when they ride\n#       along with --text; read surfaces them as a deviation-only <!-- docx:p … space-after=\"6pt\" --> hint.\n#   edit --tabs right   fix a line whose tabbed-over content WRAPS (read flags it as `docx:layout … warn`,\n#       and prints ONE consolidated fix-all summary at the top): swaps the fragile LEFT tab for a RIGHT tab\n#       flush at the margin so a long value (e.g. a city) never wraps. Rides along with --text, works\n#       per-entry in --batch, and on a RANGE (edit --at pN-pM --tabs right) cures every tab line at once.\n#   edit --text \"\"      REMOVES the line (same as `delete`; a table cell's last paragraph is blanked, not\n#       deleted, so the cell stays valid). In --batch, `{\"at\":\"pN\",\"text\":\"\"}` or `{\"at\":\"pN\",\"delete\":true}`\n#       removes a line — so a form-fill is ONE sweep: fill the cells with values, drop the leftover\n#       placeholder lines. Use `--runs '[]'` to blank a paragraph but keep an empty spacer. (Empty\n#       `--text` can't ride along with --clear/run-formatting/--style/--alignment/--tabs — those exit\n#       with a USAGE error; use `--runs '[]'` to keep a formatted empty spacer instead.) A SPAN's\n#       `--text \"\"` (pN:S-E) still deletes just those characters.\n#   --runs '[{\"type\":\"text\",\"text\":\"X\",\"bold\":true}]'\n#   --text-file PATH                               # (insert/create) LITERAL multi-paragraph text, NOT parsed — every char verbatim,\n#       each newline = a new paragraph. For prose GFM would corrupt: \"3. note\" stays \"3.\", *x* / [t](u) / bare URLs / {++x++} untouched.\n#   --markdown \"...\" | --markdown-file PATH        # GFM + math + CriticMarkup + inline HTML formatting → blocks\n#   --code \"...\" | --code-file PATH [--language LANG]\n#   --equation \"x^2 + y^2\" [--display]   (insert; edit also accepts --inline)\n#   --clear bold,italic,highlight,color,size,font,…|all   (edit; strip run formatting, keep text)\n#   --bold --italic --underline --strike --color HEX --highlight NAME --shade HEX --font NAME --size PT\n#       --caps --smallcaps --superscript --subscript   (edit; SET run formatting on EXISTING text —\n#       the inverse of --clear. Alone they format a span/paragraph/range in place; with --text they\n#       fill AND format. Like --clear, applied directly — not recorded as a tracked change.)\n#       NOTE: in a single no-content call (or one --batch entry) these run-format SET flags and the\n#       paragraph properties (--style/--alignment/--space-*/--line-spacing/--indent-*/--first-line/\n#       --hanging/--tabs) can't ride together — use separate calls/entries, or add --text to set both.\n#   --task checked|unchecked | --list bullet|ordered [--list-level N]   (insert)\n#   --task checked|unchecked                                            (edit, flip in place)\n#   --table --rows N --cols N [--widths \"A,B,C\"] [--table-width V] [--borders S] [--layout L]   (insert)\n#   --image SRC [--alt T] [--width IN] [--height IN] [--caption \"Figure 1: …\"]   (insert; SRC = path, data: URI, or http(s) URL; --caption adds a Word \"Caption\"-styled line under the figure)\n#   --page-break | --column-break | --section [--columns N] [--type T]   (insert)\n\ndocx comments add     FILE --at LOCATOR --text \"...\" [--author NAME] [--current | --baseline]\ndocx comments add     FILE --anchor \"phrase\" --text \"...\" [--occurrence N]\ndocx comments add     FILE --batch reviews.jsonl                    # JSONL: { at | anchor (+occurrence), text, author? }\ndocx comments reply   FILE --at cN --text \"...\"\ndocx comments resolve FILE --at cN [--at cM ...] [--unset] | --batch resolutions.jsonl\ndocx comments delete  FILE --at cN [--at cM ...]          | --batch removals.jsonl\n\ndocx footnotes add    FILE --at pN[:offset] (--text \"...\" | --runs JSON | --markdown TEXT)\ndocx footnotes edit   FILE --at fnN (--text \"...\" | --runs JSON | --markdown TEXT)\ndocx footnotes delete FILE --at fnN\ndocx endnotes  add    FILE --at pN[:offset] (--text \"...\" | --runs JSON | --markdown TEXT)\ndocx endnotes  edit   FILE --at enN (--text \"...\" | --runs JSON | --markdown TEXT)\ndocx endnotes  delete FILE --at enN\n\n# Headers & footers (one shared impl — \"marginals\"). Default placement is every\n# page, all sections (--at sN targets one). Content: ONE primary source, except\n# --text + one field = two-zone (text left, field right at a content-edge tab).\ndocx headers set   FILE [--at sN] [--type default|first|even | --first-page | --even | --odd] \\\n                        [--text \"...\"] [--align left|center|right] \\\n                        [--page-number [--of-pages] | --date [--date-format FMT] | --style-ref STYLE | --field filename|title|author] \\\n                        [--track] [--author NAME]\ndocx headers clear FILE [--at sN] [--type T | --first-page | --even | --odd]\ndocx footers set   FILE …   # identical flags, kind=footer (e.g. --page-number --of-pages → \"Page X of Y\")\ndocx footers clear FILE …\n\ndocx images extract FILE --to DIR [--at imgN]            # --to = output directory; --at picks one image\ndocx images replace FILE --at imgN --with ./new.png\ndocx images delete  FILE --at imgN\n\ndocx hyperlinks add     FILE --at pN:S-E --url URL\ndocx hyperlinks replace FILE --at linkN --with URL\ndocx hyperlinks delete  FILE --at linkN\n\ndocx tables insert-row    FILE --at tN [--position INDEX] [--cells \"a,b,c\"]\ndocx tables delete-row    FILE --at tN:rR\ndocx tables insert-column FILE --at tN [--position INDEX] [--width TWIPS]\ndocx tables delete-column FILE --at tN:cC\ndocx tables set-widths    FILE --at tN --widths \"25%,25%,50%\" | \"1440,...\" | auto\ndocx tables merge         FILE --at tN:rR1cC1-rR2cC2\ndocx tables unmerge       FILE --at tN:rRcC\ndocx tables borders       FILE --at tN [--style single|double|none] [--size N] [--color HEX]\ndocx tables format        FILE --at LOCATOR [--shade HEX|NAME] [--valign top|center|bottom]\n                          [--halign left|center|right|justify] [--cell-borders SIDES]\n                          [--align left|center|right] [--style ID] [--row-height M] [--repeat-header]\n\ndocx lists set            FILE --at pN [--start N] [--format FMT] [--restart] [--continue]\n                          # renumber a NUMBERED list (--at = any item; applies to the whole list).\n                          # FMT = decimal | lower-alpha | upper-alpha | lower-roman | upper-roman.\n                          # --restart splits a fresh list off here; --continue picks up the previous\n                          # list's numbering instead of restarting. Untracked (Word records no revision).\n\ndocx track-changes on|off FILE\ndocx track-changes list   FILE [--json]\ndocx track-changes accept FILE (--at tcN [--at tcM ...] | --at revN | --all)\ndocx track-changes reject FILE (--at tcN [--at tcM ...] | --at revN | --all)\ndocx track-changes apply  FILE [--accept H ...] [--reject H ...]\n# `list` defaults to a text table, one LOGICAL change per line (revN collapses a del+ins\n# pair onto one line); `--json` for the raw array. A del+ins REPLACE pair shares a\n# \"group\": \"revN\"; `--at revN` accepts/rejects both halves in one call.\n# To FINALIZE a review (accept some, reject the rest), use `apply` — it takes both decision\n# lists in ONE call, resolved against the original ids, so nothing renumbers mid-operation\n# and the file is never left half-finalized. Doing it as separate accept then reject calls\n# renumbers the ids between them. After a subset accept/reject/apply, the confirmation\n# re-lists what remains with its renumbered handles.\n```\n\nOne rule to memorize: addressing an existing thing is always`--at`\n\n.`comments reply/resolve/delete`\n\n,`footnotes/endnotes edit/delete`\n\n,`images extract/replace/delete`\n\n,`hyperlinks replace/delete`\n\n,`tables *`\n\n,`track-changes accept/reject`\n\n,`edit`\n\n, and`delete`\n\nall take`--at LOCATOR`\n\n. The exceptions are positional or directional by nature:`insert`\n\nuses`--after`\n\n/`--before LOCATOR`\n\n(or`--at-start`\n\n/`--at-end`\n\nfor the document boundaries, no locator);`read`\n\nslices with`--from`\n\n/`--to LOCATOR`\n\n;`wc`\n\ntakes a positional`[LOCATOR]`\n\n;`find`\n\n/`replace`\n\ntake a positional`QUERY`\n\n/`PATTERN`\n\n(and`replace`\n\naccepts an optional`--at pN`\n\ntoconfinethe substitution to one paragraph).`images extract --to DIR`\n\nis anoutput directory, not a locator.\n\nThe CLI is built for non-interactive agents. **Exit code is the success signal**, output is data:\n\n| Exit | Meaning | Error codes |\n|---|---|---|\n`0` |\nsuccess | — |\n`2` |\nusage / bad locator | `USAGE` , `INVALID_LOCATOR` |\n`3` |\naddressed thing not found | `FILE_NOT_FOUND` , `PART_NOT_FOUND` , `BLOCK_NOT_FOUND` , `COMMENT_NOT_FOUND` , `IMAGE_NOT_FOUND` , `HYPERLINK_NOT_FOUND` , `TRACKED_CHANGE_NOT_FOUND` , `MATCH_NOT_FOUND` |\n`1` |\ngeneral failure | `NOT_A_ZIP` , `TRACKED_CHANGE_CONFLICT` , `TABLE_STRUCTURE` , `IMAGE_SOURCE` , `RENDER_ENGINE` , `RENDER_FAILED` , `UNHANDLED` |\n\n**Errors** print `{code, error, hint?}`\n\nJSON to stdout with a nonzero exit — note there is **no ok field**; the exit code plus\n\n`code`\n\nare the unambiguous signal.**The ok field appears in exactly one place: the --verbose success ack** (\n\n`{ok:true, operation, path, …}`\n\n). Without `-v`\n\n, success output is shaped for the next command:| Command class | Default stdout on success | `--verbose` |\n|---|---|---|\nMutator that mints a new handle — `comments add` →`cN` , `comments reply` →`cN` , `footnotes/endnotes add` →`fnN` /`enN` , `hyperlinks add` →`linkN` , `insert` →the new `pN` |\nthe bare locator(s), one per line (a multi-block `--markdown` insert prints several) |\nfull `{ok:true,…}` ack |\nMutator with no new handle — `edit` , `delete` , `replace` , `create` , `comments resolve/delete` , `images replace/delete` , `hyperlinks replace/delete` , `footnotes/endnotes edit/delete` , `headers/footers set/clear` , `tables *` , `track-changes accept/reject` & toggle |\none-line confirmation — `<operation> <target>` (e.g. `edit t1:r0c1:p0` , `edit 7 changes` , `replace 3 occurrences replaced` ) (exit `0` ) |\nfull `{ok:true,…}` ack |\n`find` |\nmatched span locators, one per line (no matches → nothing, exit `0` ) |\n`--json` → `{ totalMatches, query, view, matches:[…], normalizedQuery? }` |\n`wc` |\nthe bare count (whole-doc adds a tab-separated `sections` column, like `wc` ) |\n`--json` → `{ words, scope, view, sections? }` |\n`outline` |\nindented `LOCATOR⇥TEXT` tree (two spaces per level) |\n`--json` → nested `[{ id, locator, level, style, text, children }]` |\n`read` |\nGFM Markdown; each paragraph carries its `pN` locator once — a trailing bare `<!-- pN -->` on plain paragraphs, or the leading token of its `<!-- docx:p pN … -->` note when one is emitted |\n`--ast` → the JSON AST body (`docx info schema` ) |\n`render` |\nimage paths, one per line | `--verbose` → `{ok, operation, path, engine, output, pages}` |\n`* list` (all eight `list` verbs) |\na bare JSON array; each item's `id` is its `--at` handle |\n— |\n\n`--dry-run`\n\nalways prints a preview object (no `ok`\n\n) and writes nothing; it wins over `-o/--output`\n\n.\n\nLocators come in two flavors. **Positional block ids** (`pN`\n\n, `tN`\n\n, `sN`\n\n) are derived from document order and **shift after structural edits** — re-read between non-trivial mutations. **Entity ids** (`cN`\n\n, `imgN`\n\n, `linkN`\n\n, `fnN`\n\n, `enN`\n\n, `tcN`\n\n, `eqN`\n\n) are surfaced by a `list`\n\nverb (or `read --ast`\n\n) and are what you pass to `--at`\n\n:\n\n| Id | Discover with | Used by |\n|---|---|---|\n`pN` / `tN` / `sN` (block ids) |\n`docx read FILE` (the `<!-- pN -->` trailers), `docx read FILE --ast` , `docx outline FILE` (heading `pN` s), or `docx render` page images |\n`read` , `edit` , `insert` , `delete` , `wc` , `find` results |\n`cN` (comment) |\n`docx comments list FILE` |\n`comments reply/resolve/delete --at` |\n`fnN` / `enN` (foot/endnote) |\n`docx footnotes list FILE` / `docx endnotes list FILE` |\n`footnotes/endnotes edit/delete --at` |\n`hdrN` / `ftrN` (header/footer) |\n`docx headers list FILE` / `docx footers list FILE` (or `read --ast` ) |\naddressed by section+type, not the id: `headers/footers set --at sN --type T` |\n`imgN` (image) |\n`docx images list FILE` |\n`images extract/replace/delete --at` |\n`linkN` (hyperlink) |\n`docx hyperlinks list FILE` |\n`hyperlinks replace/delete --at` |\n`tcN` (tracked change) |\n`docx track-changes list FILE` |\n`track-changes accept/reject --at` |\n`eqN` (equation) |\n`docx read FILE --ast` (run `latex` field) |\n`edit --at eqN --equation` |\n\nEach `list`\n\nverb prints a bare JSON array where every item's `id`\n\nis exactly the handle you feed back to `--at`\n\n— pipe through `jq`\n\nto filter (`docx comments list doc.docx | jq '.[] | select(.author==\"Jane\")'`\n\n).\n\n`docx info locators`\n\n(`--json`\n\nfor machine-readable) is the canonical reference. The grammar in brief:\n\n```\npN              paragraph N                  pN:S-E          chars S..E within paragraph N\npN-pM           whole-paragraph range        pN:S-pM:E       cross-paragraph character range\nsN              section break N              tN              table N\ntN:rRcC         cell at row R, col C         tN:rRcC:pK      paragraph K of that cell (chainable)\ntN:rR / tN:cC   table row R / column C       tN:rR1cC1-rR2cC2  rectangular cell region (merge)\ncN  imgN  linkN  fnN  enN  tcN  eqN          entity ids (comment / image / hyperlink /\n                                             footnote / endnote / tracked-change / equation)\n```\n\n**Offset semantics: character offsets are 0-based, start-inclusive, end-exclusive** — `p3:5-20`\n\nis the 15 characters at indices 5..19 of paragraph 3. Offsets count the *visible* text of the paragraph in the selected view (accepted by default).\n\n**Nested tables chain the same syntax** arbitrarily deep — `t0:r2c1:t0:r0c0:p0`\n\nis the first paragraph of the (0,0) cell of the first table nested inside the (2,1) cell of the document's first table.\n\n**Not every command accepts every form** — each command's `--at`\n\n/`--from`\n\n/positional help lists exactly what it takes. The shapes:\n\n| Form | Accepted by |\n|---|---|\n`pN` , `tN` , `sN` , `tN:rRcC:pK` (blocks) |\n`read --from/--to` , `insert --after/--before` , `wc` , `comments add` |\n`pN` , `pN:S-E` , `pN-pM` , `sN` , `eqN` , `tN:rRcC:pK` , `tN:rRcC:pK:S-E` |\n`edit --at` (span/cell forms strip or replace just that range) |\n`pN` , `pN-pM` , `tN` , `sN` , `tN:rRcC:pK` |\n`delete --at` |\n`pN:S-E` , `pN:S-pM:E` , `tN:rRcC:pK:S-E` (spans) |\n`comments add --at` , `hyperlinks add --at` (single paragraph), `find` /`wc` results |\n`pN[:offset]` (point) |\n`footnotes/endnotes add --at` |\n`cN` / `fnN` / `enN` / `imgN` / `linkN` / `tcN` (entities) |\nthe matching noun's `--at` (the `c` /`fn` /`en` /`img` /`link` /`tc` prefix is optional) |\n`tN` , `tN:rR` , `tN:cC` , `tN:rRcC` , `tN:rR1cC1-rR2cC2` |\nthe `tables` verbs |\n\n**find → comment.** `find`\n\nemits bare locators that drop straight into `comments add --at`\n\n(same default view, so offsets line up — no coordinate translation):\n\n```\ndocx comments add doc.docx --at \"$(docx find doc.docx 'fatally flawed' | head -1)\" \\\n    --text \"Cite a source here?\"\n# or anchor by phrase directly:\ndocx comments add doc.docx --anchor \"fatally flawed\" --text \"Cite a source here?\"\n```\n\n**read → edit markdown round-trip.** `read`\n\nemits a markdown dialect that `edit --markdown`\n\nre-parses, so render → LLM-rewrite → splice-back is lossless for paragraphs/lists/quotes:\n\n``` php\ndocx read doc.docx --from p3 --to p3              # → markdown (with <!-- p3 --> trailer)\n# … hand to an LLM, get a revised block back …\ndocx edit doc.docx --at p3 --markdown-file revised.md   # multi-block source expands naturally\n```\n\nUse `--markdown-file`\n\n(not `--markdown TEXT`\n\n) when the source starts with `-`\n\n— Node's `parseArgs`\n\nrejects leading-dash flag values.\n\n**track-changes review loop.** Toggle tracking on, make edits (they auto-emit `<w:ins>`\n\n/`<w:del>`\n\n), then inventory and resolve:\n\n```\ndocx track-changes doc.docx on\ndocx replace doc.docx \"old phrasing\" \"new phrasing\" --all\ndocx track-changes list doc.docx                  # → JSON array of { id:tcN, kind, author, text, … }\ndocx read doc.docx --current                       # → CriticMarkup {++ins++}[^tcN] / {--del--}[^tcN]\ndocx track-changes accept doc.docx --at tc0 --at tc2   # or --all\n```\n\n`read`\n\nhas three tracked-change views: default ** --accepted** renders clean text — drops subtractive edits and inlines additive ones (the post-accept document);\n\n**shows CriticMarkup with**\n\n`--current`\n\n`[^tcN]`\n\nfootnotes; **does the reverse of accepted (the pre-change document).**\n\n`--baseline`\n\n`find`\n\n, `replace`\n\n, `wc`\n\n, and `comments add`\n\nhonor the same `--accepted`\n\n/`--baseline`\n\n/`--current`\n\nflags so offsets stay consistent across commands. Add `--comments`\n\nto `read`\n\nto append `[^cN]`\n\nfootnotes for comment spans.**In-place XML mutation.** The AST returned by `read`\n\nis a *view* over the parsed XML tree, not a separate model. When you `edit`\n\nor `comments add`\n\n, the CLI mutates the underlying XML nodes directly and serializes back. Anything not modeled in the AST (custom styles, theme colors, schema extensions) survives because untouched regions are never re-emitted. Never delete a relationship something still references — that corrupts the file — so part/relationship pruning is gated on a reference scan; unreferenced orphans are left in place.\n\n**JSX for emitters.** Constructing OOXML fragments imperatively (`<w:rPr>`\n\n→ `<w:b/>`\n\n→ `<w:color w:val=\"800080\"/>`\n\n) is verbose, so fresh XML is authored in JSX with a custom factory: `<w.rPr><w.b/><w.color w-val=\"800080\"/></w.rPr>`\n\nbecomes the right `XmlNode`\n\ntree.\n\n**Span-aware comments & hyperlinks.** `comments add --at p3:5-20`\n\n(and `hyperlinks add`\n\n) find the runs containing offsets 5 and 20, split them at the boundaries (preserving `<w:rPr>`\n\non both halves), and insert markers between the slices. Comments authored by older tools that lack `w14:paraId`\n\n(required by `commentsExtended.xml`\n\n) get a fresh paraId injected automatically on resolve/reply.\n\n**Tracked changes.** With `<w:trackChanges/>`\n\nset, `insert`\n\n/`edit`\n\n/`delete`\n\n/`replace`\n\nemit native `<w:ins>`\n\n/`<w:del>`\n\n(attributed via `--author`\n\n, `$DOCX_AUTHOR`\n\n, or `Reviewer`\n\n); pass `--track`\n\nto one of those commands (or the `tables`\n\nverbs / `images delete`\n\n) to track just that invocation even when the doc toggle is off. `edit --at pN --text`\n\nruns a word-level diff so unchanged words keep their formatting and only changed words are wrapped — the same shape Word produces mid-tracking. `accept`\n\n/`reject`\n\nhandle run-level ins/del/moveFrom/moveTo, `sectPrChange`\n\n, paragraph-mark ins/del, and the table-structural revisions (rowIns/rowDel, cellIns/cellDel, tblGridChange, tcPrChange). OOXML has no tracked-change construct for hyperlink edits or image swaps, so under tracking those emit a `[docx-cli]`\n\naudit comment instead of a fake revision (image *deletion* is honest removal — it wraps a real `<w:del>`\n\n).\n\n**Rich content.** Images insert from a path, `data:`\n\nURI, or `http(s)`\n\nURL (bounded fetch; HEIC→JPEG transcode; SVG sanitized; non-public/metadata addresses refused at every redirect hop). Equations round-trip OOXML `<m:oMath>`\n\n↔ LaTeX (reconstructed, not legacy plaintext) — authored via temml (LaTeX→MathML) plus an in-house MathML→OMML adapter, no LGPL deps. Code blocks emit one `CodeBlock`\n\n-styled paragraph per line with optional lowlight syntax highlighting (37 bundled languages); they collapse back to a GFM fenced block on read. GFM task lists round-trip Word's checkbox content control (and the Word-for-Web Wingdings-glyph variant), surfacing as `taskState`\n\nin the AST. Tables operate on a merge-aware logical grid so `gridSpan`\n\n/`vMerge`\n\ncells map onto physical `<w:tc>`\n\n, and structural edits refuse to bisect an existing merge.\n\n**Markdown dialect.** `create --from`\n\n, `insert/edit --markdown`\n\n, and the note bodies all parse the same GFM + math + CriticMarkup + inline-HTML-formatting dialect (remark + remark-gfm + remark-math + an in-house inline-surgery transform), composing the existing OOXML emitters. `read`\n\nemits a compatible dialect, so the read → edit → write loop round-trips (lossless for paragraphs, lists, and nested blockquotes; code/tables/math/headings inside a blockquote intentionally escape to top level on import). `read --ast`\n\nis the fully lossless JSON form.\n\n**Literal text — the parser-free channel.** When you want prose inserted *exactly* — reviewer notes, quoted excerpts, anything where Markdown would misfire — use `create --text-file PATH`\n\n/ `insert --text-file PATH`\n\n(or `-`\n\nfor stdin). Every character lands verbatim and each newline starts a new paragraph; nothing is interpreted, so `3. note`\n\nstays `3.`\n\n(no ordered-list renumber), and `*x*`\n\n, `[t](u)`\n\n, bare URLs, and `{++x++}`\n\nare kept as written. This exists because GFM corruption isn't always escapable: bare URLs autolink with no escape sequence at all, and CriticMarkup eats `{++…++}`\n\nregardless of backslashes — so a literal path is the only safe way to author untouched prose.\n\n**Document-wide font.** `docx styles set-default-font FILE \"Times New Roman\"`\n\nsets the font in the two places a font actually lives — `word/styles.xml`\n\n`<w:docDefaults>`\n\n(the formal default) *and* the theme font scheme (`word/theme/theme1.xml`\n\n, major + minor `<a:latin>`\n\n), since real Word docs resolve their fonts *through* the theme and touching only one silently loses to the other. Body text and theme-following headings both adopt it; styles or runs that pin their own font (a code block's monospace, a deliberately-Arial run) are preserved and named in the ack, with `--all`\n\nto repoint even those. `--size N`\n\nsets the default size on the same write.\n\n**Edit & create styles.** `docx styles set FILE --at Heading1 --color 1F4E79 --size 16 --bold`\n\nrewrites the *style definition* in `word/styles.xml`\n\n, so every paragraph or run that uses the style picks up the change at once (\"make all Heading 1s green\") — the same run-/paragraph-formatting flags as `edit`\n\n(color/font/size/highlight/underline/caps + alignment/spacing/indentation for paragraph styles). `docx styles create FILE Callout --color C00000 --bold`\n\nmints a new paragraph or character style that `insert/edit --style Callout`\n\ncan then apply. Editing an un-materialized built-in (`--at Heading3`\n\non a doc that never used it) provisions it first; a paragraph with its own *direct* formatting keeps it (the override wins — the definition edit never touches the body). Style edits are **not** tracked changes even under track-changes — matching Word, which applies style-definition edits to `styles.xml`\n\ndirectly with no redline.\n\n**List numbering.** `docx lists set FILE --at p12 --start 5`\n\nmakes a numbered list begin at 5; `--format upper-roman`\n\n(or `lower-alpha`\n\n/`upper-alpha`\n\n/`lower-roman`\n\n) switches the glyph; `--restart`\n\nsplits a fresh list off at that item; `--continue`\n\nmakes a list pick up the previous list's numbering instead of restarting. `--at`\n\nnames any item — the change applies to the whole list. The start round-trips through the markdown ordinal (the body reads `5. 6. 7.`\n\n); the glyph and any continue link, which GFM can't express, surface as a deviation-only `<!-- docx:list p12 format=\"upper-roman\" -->`\n\n/ `<!-- docx:list p20 continues -->`\n\nhint (dropped on import, like every `docx:`\n\nnote — `read --ast`\n\ncarries `list.start`\n\n/`list.format`\n\nlosslessly). Untracked, matching Word, which records no revision for a list-numbering change.\n\n**Run formatting beyond bold/italic.** Properties markdown has no native syntax for — text color, theme color, highlight, shading, underline (all 18 styles + color), super/subscript, small/all caps, font, and size — are emitted as the **HTML a markdown reader actually renders**, so the output looks right in GitHub, VS Code, Obsidian, and browsers (Pandoc `[text]{…}`\n\nspans render as literal brackets in all of those). `read`\n\nemits semantic tags where they exist — `<mark>overdue</mark>`\n\n, `<sup>x</sup>`\n\n, `<sub>2</sub>`\n\n— a `<span style=\"color:#C00000\">…</span>`\n\nfor the CSS-expressible properties, and `data-*`\n\nattributes for the OOXML-only ones CSS can't express (theme colors, underline styles); `insert/edit --markdown`\n\nparses them back losslessly, and a leading `<!-- docx:base font=\"Arial\" size=\"8pt\" -->`\n\nnote declares the document's dominant font/size once so the body isn't buried in per-run repetition. Bold/italic/strike/code/links stay native (`**`\n\n/`*`\n\n/`~~`\n\n/```\n\n/`[](…)`\n\n). Because the inline-surgery transform scans whole sibling sequences, a CriticMarkup marker or span can straddle other formatting — `{++**bold insertion**++}`\n\nis tracked correctly. An invalid enum value (e.g. a bogus highlight name) fails with a clear error rather than silently vanishing. Inserted plain content inherits the surrounding paragraph's font/size so it blends in.\n\n**Visual verification.** `docx render`\n\nis the only command that needs an external runtime: it drives Microsoft Word (macOS via `osascript`\n\n, Windows via PowerShell COM — the ground-truth renderer) or LibreOffice (`soffice`\n\n, cross-platform) to produce a PDF, then rasterizes in-process via the bundled `@hyzyla/pdfium`\n\nWASM package — no poppler/pdftoppm/ImageMagick needed. Agents that consume PNGs use this to verify edits, diff accept/reject before-vs-after, or generate screenshots.\n\n**Runtime**: Bun (`node:util`\n\nparseArgs, JSX with custom factory, native zlib)**Parser**:+`jszip`\n\n+`fast-xml-parser`\n\n`fast-xml-builder`\n\n**Markdown**:+`unified`\n\n+`remark-parse`\n\n+`remark-gfm`\n\n`remark-math`\n\n**Math**:(MIT) compiles LaTeX → MathML; an in-house MathML → OMML adapter handles the OOXML side bidirectionally`temml`\n\n**Render**:(MIT wrapper + Apache-2.0 PDFium-as-WASM) for the PDF → PNG/JPG step, plus`@hyzyla/pdfium`\n\n/`pngjs`\n\nfor image encoding`jpeg-js`\n\n**Images**:(wasm libheif) transcodes HEIC/HEIF input to JPEG on insert`heic-convert`\n\n**Quality**: Biome + Knip + tsc; LibreOffice headless for round-trip integration tests** Standard**: ECMA-376 Part 1 §17 (WordprocessingML), Transitional profile\n\nSee [CONTRIBUTING.md](/kklimuk/docx-cli/blob/main/CONTRIBUTING.md) for development setup, architecture overview, and CI.", "url": "https://wpnews.pro/news/show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens", "canonical_source": "https://github.com/kklimuk/docx-cli", "published_at": "2026-07-07 18:19:00+00:00", "updated_at": "2026-07-07 18:29:42.322891+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Docx-CLI", "Claude", "Codex", "Common Paper", "Bun"], "alternates": {"html": "https://wpnews.pro/news/show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens", "markdown": "https://wpnews.pro/news/show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens.md", "text": "https://wpnews.pro/news/show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens.txt", "jsonld": "https://wpnews.pro/news/show-hn-docx-cli-agents-read-edit-word-docs-using-1-2-the-time-and-tokens.jsonld"}}