CLI¶
wordlive ships a Click-based CLI designed for LLM tool-use loops: JSON in,
JSON out, deterministic exit codes, one structured object per invocation on
stdout. The CLI is a thin wrapper over the Python API — same
politeness, same atomic-undo.
Global flags¶
| Flag | Default | Purpose |
|---|---|---|
--json/--text |
--json |
Output format. --text prints a per-command human form (indented outline tree, bare text for reads, one-line acks for writes); JSON stays the LLM-friendly default. |
--doc DOC_NAME |
active doc | Target a specific open document by name (e.g. Report.docx). |
-h, --help |
— | Show help for the command or subgroup. |
Exit codes¶
The CLI's error boundary classifies every WordliveError into a
deterministic exit code so an LLM tool-use loop can branch on the failure
mode without parsing strings:
| Code | Meaning | Source exception |
|---|---|---|
0 |
OK | — |
1 |
Other / unclassified | WordliveError (default), DocumentNotFoundError, ImageSourceError |
2 |
Anchor or style missing | AnchorNotFoundError / StyleNotFoundError (also used for zero-match find/replace --find) |
3 |
Word busy / modal | WordBusyError (retryable) |
4 |
Word not running | WordNotRunningError |
5 |
Ambiguous match | AmbiguousMatchError (multiple find hits without --all/--occurrence) |
See the Errors page for the full exception taxonomy and retry guidance.
status¶
List all open documents and mark which is active. Each entry carries a name
(always non-empty — Document1 for a document never saved), the on-disk path
(empty until the document is saved), a saved flag, and is_active.
$ wordlive status
[{"name": "Report.docx", "path": "C:\\Users\\me\\Report.docx", "saved": true, "is_active": true},
{"name": "Document2", "path": "", "saved": false, "is_active": false}]
Failures: 4 if Word isn't running (returns [] to stdout and the error
on stderr). Useful as a probe before issuing other commands.
outline¶
Heading outline of the target document, with addressable anchor IDs. Pass
--all to list every paragraph (headings and body text and list items)
as para:N — identical to paragraphs.
$ wordlive outline
[{"level": 1, "text": "Introduction", "anchor_id": "heading:1"},
{"level": 2, "text": "Context", "anchor_id": "heading:3"},
{"level": 1, "text": "Risks", "anchor_id": "heading:8"}]
This is the entry point for LLM workflows that need to discover what's
addressable in the document. The emitted anchor_id strings are exactly
what replace, go-to, insert, and exec consume.
paragraphs¶
List every paragraph in document order — headings, body text, and list
items alike — each with a para:N anchor, its outline level, an
is_heading flag, character start/end offsets, and its text. outline
--all is an alias.
$ wordlive paragraphs
[{"index": 1, "anchor_id": "para:1", "level": 1, "is_heading": true, "start": 0, "end": 13, "text": "Introduction"},
{"index": 2, "anchor_id": "para:2", "level": 10, "is_heading": false, "start": 13, "end": 29, "text": "Body text here."},
{"index": 3, "anchor_id": "para:3", "level": 2, "is_heading": true, "start": 29, "end": 35, "text": "Risks"}]
para:N shares its index space with heading:N — paragraph 1 is both
para:1 and (because it's a heading) heading:1. The emitted offsets feed a
range:START-END target for an offset-precise, mid-paragraph
insertion via replace.
read bookmark NAME¶
Read the text of a bookmark.
Failures: 2 if the bookmark doesn't exist.
read cc NAME¶
Read the text of a content control. NAME matches the control's Title
first, then Tag.
Failures: 2 if no content control with that Title/Tag exists.
read section HEADING¶
wordlive read section HEADING [--doc DOC_NAME]
wordlive read section --anchor-id heading:N [--doc DOC_NAME]
Read the body text under a heading — from the end of the heading paragraph up to the next heading whose level is less than or equal to this one's (or to the end of the document if there's no such boundary).
$ wordlive read section "Introduction"
{"heading": "Introduction",
"anchor_id": "heading:1",
"level": 1,
"text": "This document covers the Q2 risk register …"}
$ wordlive --text read section "Introduction"
This document covers the Q2 risk register …
--text mode emits only the section body — handy for piping into an LLM
prompt without ceremony. Use --anchor-id heading:N to disambiguate when the
same visible heading text appears more than once.
Failures: 2 heading not found.
write bookmark NAME --text "…"¶
Replace a bookmark's text inside a single atomic-undo scope.
$ wordlive write bookmark Address --text "456 Elm St"
{"ok": true, "anchor": {"kind": "bookmark", "name": "Address"}}
The bookmark is preserved — wordlive re-adds it covering the new content
after the Word Range.Text assignment (which would otherwise delete it).
Failures: 2 anchor not found, 3 Word busy.
write cc NAME --text "…"¶
Replace a content control's text inside a single atomic-undo scope.
$ wordlive write cc Signatory --text "Jane Doe"
{"ok": true, "anchor": {"kind": "content_control", "name": "Signatory"}}
Failures: 2 anchor not found, 3 Word busy.
insert --anchor-id ID --text "…"¶
wordlive insert --anchor-id ID --text "..." [--before | --after] [--style "Body Text"] [--doc DOC_NAME]
Insert a new paragraph relative to any anchor — addressed the same way
every other command addresses things, with --anchor-id (heading:N,
para:N, bookmark:NAME, a cell, a range). --after (the default) lands the
new paragraph just below the anchor; --before lands it just above. --after
works even when the anchor is the document's last paragraph — the new paragraph
is appended before the final mark — so you can build a document top-down from a
single empty paragraph.
$ wordlive insert --anchor-id heading:8 --text "New risk identified."
{"ok": true, "anchor_id": "heading:8", "where": "after", "style": null}
$ wordlive insert --anchor-id para:3 --text "Section preamble." --before
{"ok": true, "anchor_id": "para:3", "where": "before", "style": null}
--style is optional; if given it must be a Word style name that exists in
the document — the style is validated before the paragraph is inserted, so a
typo never partially mutates the document. Use wordlive style list to see
the available names. Failures: 2 anchor not found or style not found, 3
Word busy.
To insert text inside a paragraph at a precise offset rather than as a new
paragraph, target a collapsed range instead — replace --anchor-id
range:120-120 --text "…" — using offsets from paragraphs or find.
insert-break --anchor-id ID [--kind …] [--before | --after]¶
wordlive insert-break --anchor-id ID
[--kind page|column|section_next|section_continuous]
[--before | --after] [--doc DOC_NAME]
Insert an explicit page, column, or section break at any anchor — the clean,
discoverable replacement for appending a paragraph whose text is a literal
form-feed. --kind defaults to page (the common case); column breaks a
multi-column layout, and the two section_* kinds start a new document section
(which can carry its own headers/footers and page setup — see section).
--after (default) drops the break just past the anchor; --before, just
before it.
$ wordlive insert-break --anchor-id para:12
{"ok": true, "anchor_id": "para:12", "kind": "page", "where": "after"}
$ wordlive insert-break --anchor-id heading:3 --kind section_next --before
{"ok": true, "anchor_id": "heading:3", "kind": "section_next", "where": "before"}
To make a style (e.g. every Heading 1) open a new page without a stray
break character that drifts on reflow, prefer format-paragraph --anchor-id ID
--page-break-before instead — it's a paragraph property, not an inserted mark.
Failures: 1 unknown --kind (usage error), 2 anchor not found, 3 Word
busy.
prepend --text "…" / append --text "…"¶
wordlive prepend --text "..." [--paragraph | --inline] [--style "Body Text"] [--doc DOC_NAME]
wordlive append --text "..." [--paragraph | --inline] [--style "Body Text"] [--doc DOC_NAME]
prepend is the mirror of append: it adds to the very start of the
document (a new first paragraph by default, or --inline to join the opening
paragraph) — equivalent to insert --anchor-id start --text "…". Everything
below applies to both; just swap "end" for "start".
$ wordlive prepend --text "DRAFT — not for distribution"
{"ok": true, "mode": "paragraph", "style": null}
Append text to the very end of the document — the high-level "end of doc"
helper, no anchor needed. --paragraph (the default) makes text a new final
paragraph; --inline continues the document's last paragraph instead. This is
exactly insert --anchor-id end --text "…" (the end anchor names the
position past the last paragraph), spelled as its own verb.
$ wordlive append --text "Closing note added by automation."
{"ok": true, "mode": "paragraph", "style": null}
$ wordlive append --text " (verified)" --inline
{"ok": true, "mode": "inline", "style": null}
--style is optional, paragraph-mode only, and must name a style that exists
in the document — it's validated before anything is written, so a typo never
partially mutates the document (wordlive style list shows the names).
Failures: 2 style not found, 3 Word busy.
insert-image --anchor-id ID (--path FILE | --base64 VALUE) --wrap WRAP¶
wordlive insert-image --anchor-id ID (--path FILE | --base64 VALUE) --wrap WRAP \
[--before | --after] [--block | --no-block] [--width N] [--height N] \
[--alt-text "…"] [--lock-aspect | --no-lock-aspect] [--doc DOC_NAME]
Embed an image at any anchor. Exactly one image source is required:
--path reads a file from disk (best for large images); --base64 takes
base64 data inline, or --base64 - reads base64 from stdin (handy when an
LLM holds image data in memory). The picture is embedded in the document, not
linked, so the source file can move or vanish afterwards. Word auto-detects the
image's natural size; --width/--height (points) override it and
--lock-aspect (the default) keeps the aspect ratio.
--wrap is required so layout intent is always explicit:
--wrap |
Effect |
|---|---|
inline |
Stays in the text flow, like a character. |
auto |
Floats: Square if ≤ half the page's usable width, else top-and-bottom. |
square tight through top-bottom front behind |
Floats with that wrap type. |
--after (default) places the image just below the anchor; --before above.
--block puts the image on its own new (Normal) line instead of in the
anchor's text run — use it with --before at a heading so the image lands on its
own line above the heading instead of joining the heading text.
$ wordlive insert-image --anchor-id heading:2 --path diagram.png --wrap auto
{"ok": true, "anchor_id": "heading:2", "anchor": {"kind": "heading", "name": "Risks"}, "wrap": "auto", "where": "after"}
$ base64 logo.png | wordlive insert-image --anchor-id bookmark:Header --base64 - --wrap square --width 96
{"ok": true, "anchor_id": "bookmark:Header", "anchor": {"kind": "bookmark", "name": "Header"}, "wrap": "square", "where": "after"}
Failures: 1 the image is missing, unreadable, or not a recognised raster
format (PNG/JPEG/GIF/BMP/TIFF) — an ImageSourceError; 2 anchor not found
or an invalid --wrap value; 3 Word busy.
snapshot [--anchor-id ID | --page N | --pages A-B]¶
wordlive snapshot [--anchor-id ID | --page N | --pages A-B] \
[--out FILE] [--dpi 150] [--doc DOC_NAME]
Render document page(s) to PNG so a vision model can see the layout — real fonts, spacing, and page geometry, not just the text. Word exports a pixel-faithful PDF of the document it has open and wordlive rasterises the requested pages. Read-only: the document and the user's cursor are untouched.
Pick at most one target; with none, the whole document is rendered:
| Target | Renders |
|---|---|
--anchor-id ID |
the page(s) the anchor occupies — a heading: expands to its whole section (heading + body) |
--page N |
a single 1-based page |
--pages A-B |
an inclusive page span, e.g. 2-4 |
Output: with --out FILE the image is written to disk — a single page to FILE,
multiple pages alongside it as <stem>-p<N><suffix>. Without --out, base64
PNG data is returned inline in the JSON (images[].base64), which suits an LLM
that wants to look at the page directly. --dpi (default 150) sets resolution.
This needs the optional snapshot extra (PyMuPDF):
pip install "wordlive[snapshot]" (or uv add "wordlive[snapshot]").
$ wordlive snapshot --anchor-id heading:3 --out section.png
{"ok": true, "selector": "heading:3", "dpi": 150, "count": 1, "images": [{"page": 4, "bytes": 81234, "path": "section.png"}]}
$ wordlive snapshot --page 1
{"ok": true, "selector": 1, "dpi": 150, "count": 1, "images": [{"page": 1, "bytes": 64210, "base64": "iVBORw0KGgo…"}]}
Failures: 1 PyMuPDF isn't installed, or rasterising the PDF failed — a
SnapshotError; 2 --anchor-id not found; 3 Word busy.
cursor read / cursor write --text "…"¶
wordlive cursor read [--doc DOC_NAME]
wordlive cursor write --text "..." [--replace | --no-replace] [--doc DOC_NAME]
The explicit cursor surface. Every other command targets a semantic anchor
and preserves the user's cursor; cursor is the deliberate exception, for when
the user genuinely wants to read or write at their current position. It is not
addressable by --anchor-id — that separation is intentional, signalling it's
the non-preferred mode.
cursor read reports the selection's start/end, whether it's collapsed
(an insertion point with no selected text), the selected text, and the
containing para:N so you can pivot back to anchored edits:
$ wordlive cursor read
{"start": 142, "end": 142, "collapsed": true, "text": "", "paragraph": {"anchor_id": "para:7"}}
cursor write types at the cursor and — unlike anchor writes — deliberately
leaves the cursor after the inserted text. With a spanning selection, the
default --replace overwrites it (like typing); --no-replace inserts at the
selection start without removing it.
Failures: 3 Word busy, 4 Word not running.
find --text "…"¶
Locate every fuzzy occurrence of the given text in the document (read-only). Matching is forgiving of cosmetic differences that show up when LLMs re-emit text — whitespace runs collapse, smart quotes/dashes fold to ASCII, NBSPs become spaces, and the strings are NFKC-normalized before comparison.
$ wordlive find --text "the risk register"
[{"anchor_id": "range:412-429",
"start": 412,
"end": 429,
"text": "the risk register"}]
Use --in ANCHOR_ID to restrict the search. Headings expand to their
section (the body under the heading), which is the common case for
"replace this phrase, but only inside the Risks section":
text in each match is the actual original substring (with smart quotes,
NBSP, etc.) — that's the form Word will preserve when you replace it.
Failures: returns [] with exit 0 for no matches; 2 if --in ANCHOR_ID
refers to a missing anchor.
replace¶
wordlive replace --anchor-id ID --text "..." [--doc DOC_NAME] # anchor mode
wordlive replace --find OLD --text NEW [--in ID] [--all|--occurrence N] [--doc DOC_NAME] # fuzzy mode
Two modes share the verb:
Anchor mode — replace an entire range¶
Replace the text at an anchor identified by anchor ID. Works across all three anchor kinds.
$ wordlive replace --anchor-id heading:3 --text "Updated section text"
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Context"}}
The response's anchor.name resolves the ID back to a human-readable name.
Failures: 2 anchor not found, 3 Word busy.
Fuzzy mode — find + replace a substring¶
Locate --find OLD (same fuzzy match as find) and replace with --text NEW.
Word's native range replacement preserves the character formatting of the
matched range — bold stays bold, italics stay italic — so you don't need to
re-state formatting on the replacement.
$ wordlive replace --find "Q1 2025" --text "Q2 2025"
{"ok": true,
"replacements": [{"anchor_id": "range:412-419",
"start": 412, "end": 419, "text": "Q1 2025"}]}
| Flag | Meaning |
|---|---|
--in ID |
Restrict search to the given anchor's range (headings expand to their section). |
--all |
Replace every match. Mutually exclusive with --occurrence. |
--occurrence N |
Replace only the Nth match (1-based). Mutually exclusive with --all. |
Failures:
- Exit
2— zero matches. Same code as anchor-not-found because the agent's recovery is identical: re-fetch state and retry. - Exit
5— multiple matches and neither--allnor--occurrencewas given. Stdout still emits a JSON payload listing all matches so the agent can pick an occurrence and retry:
{"ok": false, "error": "ambiguous_match", "find": "Q1",
"matches": [{"start": 412, "end": 414, "text": "Q1"},
{"start": 887, "end": 889, "text": "Q1"}]}
- Exit
3— Word busy.
go-to --anchor-id ID¶
Move the user's cursor to an anchor. This is the one CLI command that deliberately disturbs the user's selection — every other write preserves it.
$ wordlive go-to --anchor-id bookmark:Address
{"ok": true,
"anchor_id": "bookmark:Address",
"anchor": {"kind": "bookmark", "name": "Address"}}
--no-scroll collapses the selection at the anchor without scrolling the
view to it. Failures: 2 anchor not found, 3 Word busy.
style list¶
Enumerate every style defined in the document.
$ wordlive style list
[{"name": "Normal", "type": "paragraph", "builtin": true, "in_use": true},
{"name": "Body Text", "type": "paragraph", "builtin": true, "in_use": true},
{"name": "Heading 1", "type": "paragraph", "builtin": true, "in_use": true}]
type is one of "paragraph", "character", "table", "list". Built-in
Word styles set builtin: true; user-defined styles set false. Failures:
3 Word busy, 4 Word not running.
style apply --anchor-id ID --name NAME¶
Apply a style to the anchor's range. Atomic-undo. The style must already exist in the document — wordlive does not create styles on demand.
$ wordlive style apply --anchor-id heading:3 --name "Heading 2"
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Risks"},
"style": "Heading 2"}
Word picks paragraph- vs. character-style behaviour from the style's own
Type; you don't need to model that distinction. Failures: 2 anchor or
style not found, 3 Word busy.
format-paragraph --anchor-id ID [...]¶
wordlive format-paragraph --anchor-id ID
[--alignment left|center|centre|right|justify]
[--left-indent POINTS] [--right-indent POINTS] [--first-line-indent POINTS]
[--space-before POINTS] [--space-after POINTS]
[--page-break-before | --no-page-break-before]
[--doc DOC_NAME]
centre is accepted as a synonym for center (UK spelling).
Set paragraph-formatting properties on the anchor's range. At least one
formatting flag is required. Indent and spacing values are in points —
the unit Word's COM API uses natively for these fields.
--page-break-before forces the paragraph to begin on a new page (and
--no-page-break-before clears it) — the clean, reflow-safe way to
page-break, leaving no stray break character (contrast insert-break, which
inserts an explicit one-off break). Atomic-undo.
$ wordlive format-paragraph --anchor-id heading:3 \
--alignment center --space-before 6
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Risks"},
"applied": {"alignment": "center", "space_before": 6.0}}
Only the flags you pass are written; everything else on the paragraph is
left alone. If the anchor spans a partial paragraph (e.g., a bookmark
covering five words inside a longer paragraph), Word applies the formatting
to the enclosing paragraph — that's the COM behaviour, not a wordlive
quirk. Failures: 2 anchor not found, 3 Word busy.
table list¶
Enumerate every table in the document, in top-to-bottom order.
$ wordlive table list
[{"index": 1, "title": "Budget", "rows": 4, "columns": 3},
{"index": 2, "title": "", "rows": 2, "columns": 2}]
index is the 1-based position used to address the table and its cells.
title is the table's Title (empty string if unset). Failures: 3 Word busy,
4 Word not running.
table read INDEX¶
Read table INDEX (1-based) as a grid. Each cell carries its anchor_id
(table:N:R:C) so you can feed it straight into replace, style apply, or
format-paragraph.
$ wordlive table read 1
{"index": 1, "title": "Budget", "rows": 2, "columns": 2,
"cells": [[{"row": 1, "col": 1, "text": "Item", "anchor_id": "table:1:1:1"},
{"row": 1, "col": 2, "text": "Cost", "anchor_id": "table:1:1:2"}],
[{"row": 2, "col": 1, "text": "Travel","anchor_id": "table:1:2:1"},
{"row": 2, "col": 2, "text": "$400", "anchor_id": "table:1:2:2"}]]}
Cell text is stripped of Word's internal end-of-cell markers. To write a
cell, use its anchor id with replace:
$ wordlive replace --anchor-id table:1:2:2 --text "$450"
{"ok": true, "anchor_id": "table:1:2:2", "anchor": {"kind": "cell", "name": "table:1:2:2"}}
Failures: 2 table index out of range, 3 Word busy.
table create¶
wordlive table create --anchor-id ID --rows R --cols C
[--style NAME] [--header] [--before|--after]
[--data '[["…"],…]' | --data -] [--doc DOC_NAME]
Create a new R×C table at a position anchor (heading:, para:,
start, end, range: — not a bare table:N, which addresses an existing
table). Every other verb edits existing structure; this is how you build a table
from nothing. Atomic-undo. Reports the new table's 1-based index for an
immediate follow-up set-cell / add-row.
--data populates the cells at creation from a row-major JSON 2-D array
([[r1c1, r1c2], …]), validated against R×C up front — a short/partial
array leaves trailing cells empty; an array that overflows the grid is a clean
error (exit 1). Pass --data - to read the JSON from stdin, which sidesteps
Windows quoting/backslash fights (mirrors exec --ops -).
--style names a table style defined in the document; it defaults to the
built-in Table Grid so a new table has visible borders rather than only
faint gridlines. A style name not in the document fails (exit 2). --header
bolds the first row.
$ wordlive table create --anchor-id end --rows 3 --cols 3 --header \
--data '[["Tier","Monthly","SLA"],["Wobble","$9","best effort"],["Finch","$99","99.9%"]]'
{"ok": true, "table": 2, "rows": 3, "columns": 3}
A table appended where another already sits flush against it (e.g. two tables in a row at the end of the document) is kept distinct: Word would otherwise merge adjacent tables, so a separating paragraph is inserted automatically.
Failures: 1 bad dimensions / --data shape, 2 anchor or style not found,
3 Word busy, 4 Word not running.
table add-row¶
Append a row at the end of the table. --values is an optional JSON array of
cell values, matched to columns left-to-right (extras ignored, short lists
leave trailing cells empty). Atomic-undo.
$ wordlive table add-row --table 1 --values '["Lodging", "$600"]'
{"ok": true, "table": 1, "rows": 3}
Failures: 2 table index out of range, 3 Word busy.
table delete-row¶
Delete the 1-based row R from the table. Atomic-undo.
Failures: 2 table index or row out of range, 3 Word busy.
table delete INDEX¶
Delete table INDEX (1-based) and all its cells — the structural mirror of
table create / delete-row. Atomic-undo. The indices of any tables below it
shift down by one afterwards.
Failures: 2 table index out of range, 3 Word busy.
comment list¶
Enumerate every review comment, in document order.
$ wordlive comment list
[{"index": 1, "author": "ReviewBot", "text": "Please verify this figure.",
"scope": "$400", "done": false}]
index is the 1-based handle used by resolve and delete. scope is the
document text the comment is attached to; done is the resolved flag (always
false on Word builds older than 2013). Failures: 3 Word busy, 4 Word not
running.
comment add --anchor-id ID --text "…"¶
Attach a comment to the anchor's range. The document text is untouched —
this is the polite, side-channel alternative to rewriting a passage. Atomic-undo.
The anchor id is any of the recognised forms, including
a range:START-END from find.
$ wordlive comment add --anchor-id heading:3 --text "Please expand this." --author "ReviewBot"
{"ok": true, "anchor_id": "heading:3", "comment": {"index": 1, "author": "ReviewBot"}}
--author is optional; without it Word uses the running app's user name.
Failures: 2 anchor not found, 3 Word busy.
comment resolve --index N¶
Mark comment N (from comment list) as resolved/done. Requires Word 2013+.
Atomic-undo.
Failures: 2 index out of range, 3 Word busy.
comment delete --index N¶
Delete comment N. Remaining comments re-index, so re-list before deleting
another by index. Atomic-undo.
Failures: 2 index out of range, 3 Word busy.
track status | on | off¶
wordlive track status [--doc DOC_NAME]
wordlive track on [--doc DOC_NAME]
wordlive track off [--doc DOC_NAME]
Inspect or toggle the document's Track Changes setting. While on, every edit (yours or the user's) is recorded as a revision the user can accept or reject.
The toggle is persistent — track on leaves Word recording revisions until
track off. For a self-restoring scope, prefer the library's
doc.tracked_changes() context manager, or set "tracked": true on an
exec script to record a single batch as tracked
changes and restore the prior setting afterwards. Failures: 3 Word busy, 4
Word not running.
list show¶
Enumerate every bullet/numbered list in the document, top to bottom. Each row
carries a range:START-END anchor_id covering the whole list, so you can
feed it straight into list restart, replace, or comment add.
type is bulleted / numbered / outline / number-only / mixed.
Failures: 3 Word busy, 4 Word not running.
list apply --anchor-id ID --type …¶
Turn the anchor's paragraphs into a list. --type defaults to bulleted.
Numbering starts fresh at 1 unless --continue is given, which continues from
a list immediately above. Atomic-undo.
$ wordlive list apply --anchor-id heading:6 --type numbered
{"ok": true, "anchor_id": "heading:6",
"anchor": {"kind": "heading", "name": "Steps"},
"type": "numbered", "continue_previous": false}
Failures: 2 anchor not found, 3 Word busy.
list info --anchor-id ID¶
Report the list state at an anchor (read-only): {type, level, number, string},
where string is the rendered marker ("1.", "a)", "•"). type is
"none" when the anchor isn't in a list.
$ wordlive list info --anchor-id range:512-540
{"type": "numbered", "level": 1, "number": 3, "string": "3."}
Failures: 2 anchor not found, 3 Word busy.
list remove | restart | indent | outdent --anchor-id ID¶
wordlive list remove --anchor-id ID [--doc DOC_NAME] # strip list formatting
wordlive list restart --anchor-id ID [--doc DOC_NAME] # restart numbering at 1
wordlive list indent --anchor-id ID [--doc DOC_NAME] # demote one level (1 -> 2)
wordlive list outdent --anchor-id ID [--doc DOC_NAME] # promote one level (2 -> 1)
All four are atomic-undo. restart re-applies the list's own template starting
at 1; it errors if the anchor isn't part of a list.
$ wordlive list restart --anchor-id range:512-540
{"ok": true, "anchor_id": "range:512-540", "anchor": {"kind": "range", "name": "range:512-540"}}
Failures: 2 anchor not found, 3 Word busy.
section list¶
List the document's sections with each one's page setup.
$ wordlive section list
[{"index": 1,
"page_setup": {"orientation": "portrait",
"top_margin": 72.0, "bottom_margin": 72.0,
"left_margin": 72.0, "right_margin": 72.0,
"page_width": 612.0, "page_height": 792.0}}]
Margins and page dimensions are in points. Headers and footers live in the
header / footer commands. Failures: 3 Word busy, 4 Word not running.
header read | write · footer read | write¶
wordlive header read [--section N] [--which primary|first|even] [--doc DOC_NAME]
wordlive header write [--section N] [--which primary|first|even] --text "..." [--doc DOC_NAME]
wordlive footer read [--section N] [--which primary|first|even] [--doc DOC_NAME]
wordlive footer write [--section N] [--which primary|first|even] --text "..." [--doc DOC_NAME]
Read or set a section's header/footer. --section defaults to 1 and
--which to primary (the other options are first for the first-page
header/footer and even for even pages). write is atomic-undo. A header/footer
is just a range, so its id (header:S:WHICH / footer:S:WHICH) also works with
replace, style apply, and format-paragraph.
$ wordlive header read --section 1
{"anchor_id": "header:1:primary", "section": 1, "which": "primary", "text": "Confidential"}
$ wordlive header write --section 1 --text "ACME Corporation — Q2 Report"
{"ok": true, "anchor_id": "header:1:primary", "section": 1, "which": "primary"}
--text mode (wordlive --text header read) emits just the header text.
Failures: 2 section out of range, 3 Word busy.
exec — --script ops.json or --ops '{…}'¶
Apply a batch of operations under a single atomic-undo scope. This is the most useful command for LLM tool-use: one round-trip per intent, not one per operation.
Provide the batch one of three ways (exactly one of --script / --ops is
required):
--script ops.json— read it from a file.--ops '{"ops": [...]}'— pass the JSON inline on the command line.--ops -— read the JSON from stdin (e.g.… | wordlive exec --ops -), which sidesteps the shell's command-line length limit and is the right choice for large payloads such as inline base64 images.
In every form a bare [...] array is accepted as shorthand for {"ops": [...]},
and malformed JSON returns a clean error (exit 1) rather than a traceback.
Script shape:
{
"label": "Update report",
"ops": [
{"op": "write_bookmark", "name": "Address", "text": "123 Main St"},
{"op": "write_cc", "name": "Signatory", "text": "Jane Doe"},
{"op": "insert_paragraph", "anchor_id": "heading:8", "text": "New risk paragraph.",
"where": "after", "style": "Body Text"},
{"op": "replace", "anchor_id": "heading:3", "text": "Updated section text"}
]
}
Supported ops¶
op |
Required fields | Optional |
|---|---|---|
write_bookmark |
name, text |
— |
write_cc |
name, text |
— |
insert_paragraph |
anchor_id, text |
where (after/before) or before: true, style |
append |
text |
style |
append_inline |
text |
— |
prepend |
text |
style |
prepend_inline |
text |
— |
insert_image |
anchor_id, wrap, and one of path / base64 |
where or before: true, block, width, height, alt_text, lock_aspect |
replace |
anchor_id, text |
— |
find_replace |
find, text |
in, all, occurrence |
apply_style |
anchor_id, name |
— |
format_paragraph |
anchor_id |
alignment, left_indent, right_indent, first_line_indent, space_before, space_after, page_break_before |
set_cell |
table, row, col, text |
— |
add_row |
table |
values |
delete_row |
table, row |
— |
create_table |
anchor_id, rows, cols |
style, data (row-major 2-D), header, where or before: true (new cells default to the Normal paragraph style) |
delete_table |
table |
— |
insert_break |
anchor_id |
kind (page/column/section_next/section_continuous), where or before: true |
add_comment |
anchor_id, text |
author |
resolve_comment |
index |
— |
delete_comment |
index |
— |
apply_list |
anchor_id |
type (bulleted/numbered/outline), continue |
remove_list |
anchor_id |
— |
restart_numbering |
anchor_id |
— |
indent_list |
anchor_id |
— |
outdent_list |
anchor_id |
— |
write_header |
section, text |
which (primary/first/even) |
write_footer |
section, text |
which |
The find_replace op mirrors wordlive replace --find … — fuzzy whitespace
+ smart-quote match, optional in anchor to scope it, and either all or
occurrence to handle multi-match. Ambiguous-match failures surface in the
batch response's failure.matches so the LLM can rewrite the op and retry. To
edit text inside a table, scope the replace to the cell anchor
("in": "table:N:R:C"); a match resolved through a whole-document scope that
can't be verified raises a replace_verification failure rather than risk
overwriting the wrong cell.
insert_paragraph mirrors the insert command: a new paragraph relative to
any anchor, with placement defaulting to after and an optional style that's
validated before the batch mutates anything. Placement accepts either the
verbose "where": "before"|"after" or the boolean "before": true — the latter
mirrors the command's --before/--after flags, so the same intent encodes the
same way whether you type it or batch it. (insert_image accepts both forms too.)
append adds a new final paragraph at the very end of the document
(optional style, validated first) — no anchor to resolve, equivalent to an
insert_paragraph op targeting the end anchor. append_inline instead
continues the document's last paragraph and takes text only (no style).
prepend / prepend_inline are their start-of-document mirrors (the start
anchor). append_paragraph / prepend_paragraph remain as explicit synonyms
of append / prepend.
Any field an op doesn't recognise (a typo, or a style handed to an inline
append) is reported in a top-level warnings array on the batch result — the op
still runs, but the ignored field is surfaced rather than silently dropped, so a
successful-looking response can't mask a payload you got wrong.
insert_image mirrors insert-image. Supply the image with either a path
(read from disk) or base64 (inline data — the natural choice in a JSON op,
with no command-line length limit). wrap is required; the optional fields
match the command's flags, including block (place the image on its own new
Normal line instead of in the anchor's text run). A bad image source surfaces
as the batch's failure with type: "ImageSourceError".
apply_style and format_paragraph are the same as their dedicated CLI
verbs — the style must already exist in the document, indent and spacing
values are in points, alignment is one of left/center/right/justify.
format_paragraph's page_break_before (a bool) forces or clears a
reflow-safe page break before the paragraph — the clean way to page-break a
style without a stray break character.
set_cell, add_row, and delete_row operate on tables by 1-based table
index. set_cell is shorthand for a replace on a table:N:R:C anchor;
add_row's optional values is a JSON array matched to columns. All three
table ops join the same atomic-undo scope as the rest of the batch.
create_table builds a new table at a position anchor_id (heading:,
para:, start, end, range: — not a bare table:N); delete_table
removes one by 1-based table index. create_table's data is a row-major 2-D
array validated against rows×cols before the batch mutates anything,
style defaults to Table Grid, and header bolds the first row. Because a
successful batch reports structure it created, the response carries an
outputs array — [{"index": <op index>, "op": "create_table", "table": N,
"rows": R, "columns": C}] — so a later op (or a follow-up call) can address the
new table by its reported index. Filling the whole grid through data in the
create op keeps it one atomic undo and avoids a set_cell storm.
insert_break mirrors the insert-break command — an explicit page, column,
or section break at any anchor_id. kind defaults to page; placement
accepts the same where / before forms as the other insert ops. For a
reflow-safe page break tied to a paragraph (rather than a one-off mark), use a
format_paragraph op with page_break_before instead.
add_comment, resolve_comment, and delete_comment mirror the comment
verbs — add_comment attaches a side-channel annotation to an anchor_id
without touching the text, while resolve_comment / delete_comment take a
1-based index. Since deletes re-index, ordering matters within a batch.
apply_list, remove_list, restart_numbering, indent_list, and
outdent_list mirror the list verbs — all take an anchor_id, and
apply_list's optional type defaults to bulleted. write_header /
write_footer set a section's header/footer by 1-based section index, with
an optional which (primary / first / even, default primary) — handy
for stamping a client name or page footer across a generated document in the
same atomic-undo batch as the body edits.
Recording the batch as tracked changes¶
Set "tracked": true at the top level of the script to flip Word's Track
Changes on for the whole batch and restore the prior setting when it finishes —
so the user sees every op as an accept/reject-able revision under one Ctrl-Z:
{
"label": "Suggest rewordings",
"tracked": true,
"ops": [
{"op": "find_replace", "find": "utilise", "text": "use", "all": true}
]
}
Behaviour on partial failure¶
If any op fails, the entire scope's UndoRecord still closes cleanly — but
operations before the failure have already been applied. The user can
roll the whole batch back with one Ctrl-Z. The response reports the failure
point precisely so an LLM can retry with a corrected payload:
{
"ok": false,
"ops_run": 2,
"label": "Update report",
"failure": {
"index": 2,
"op": {"op": "insert_paragraph", "anchor_id": "heading:99", "text": "…"},
"error": "heading not found: 'heading:99'",
"type": "AnchorNotFoundError"
}
}
The exit code reflects the first failed op: 2 for anchor-not-found,
3 for Word-busy, etc. — so an LLM's retry policy can branch on it.
Example invocation¶
llm-help¶
Print a full agent guide — a bundled SKILL.md — to stdout: the anchor
model, every verb, image insertion, the exec batch format, and the exit-code
taxonomy. wordlive --help points an agent straight here, so a model can get
everything it needs in one call without an install step. Defaults to the CLI
guide; --python prints the Python-API (import wordlive as wl) guide
instead.
Unlike every other command, the output is raw Markdown rather than JSON (and is
unaffected by --json/--text) — it's documentation, exactly like --help
itself, meant to read cleanly into a model's context. The YAML frontmatter that
fronts the installed skill is stripped. Offline: it never touches Word.
$ wordlive llm-help
# wordlive (CLI)
`wordlive` drives a **running** Microsoft Word instance over COM (Windows only).
...
This is the same content install-skill writes to disk; reach
for llm-help when you just want it in context now, and install-skill when you
want coding tools to discover it on their own.
install-skill¶
Install wordlive's bundled agent skills so LLM coding tools can pick up how
to drive it. wordlive ships two: wordlive-cli (the command-line workflow) and
wordlive-python (the import wordlive as wl API). By default only the CLI
skill is installed; pass --python for just the Python one, or --both for
both. They land under the current project at ./.agents/skills/<name>/SKILL.md
(or ~/.agents/skills/<name>/ with --system). Offline — it never touches
Word, and refuses to clobber an existing file unless you pass --force.
$ wordlive install-skill
{"ok": true, "scope": "local", "installed": [
{"kind": "cli", "name": "wordlive-cli", "path": ".../.agents/skills/wordlive-cli/SKILL.md", "bytes": 6172}]}
$ wordlive install-skill --both
{"ok": true, "scope": "local", "installed": [
{"kind": "cli", "name": "wordlive-cli", "path": ".../.agents/skills/wordlive-cli/SKILL.md", "bytes": 6172},
{"kind": "python", "name": "wordlive-python", "path": ".../.agents/skills/wordlive-python/SKILL.md", "bytes": 7460}]}
$ wordlive install-skill --python --system --force
{"ok": true, "scope": "system", "installed": [
{"kind": "python", "name": "wordlive-python", "path": "/home/you/.agents/skills/wordlive-python/SKILL.md", "bytes": 7460}]}
Failures: 1 if a target can't be written (every target is checked up front, so
a missing --force fails before anything is written).
install-mcp¶
wordlive install-mcp [--client claude-desktop|claude-code] [--name NAME]
[--directory DIR] [--config PATH] [--print] [--force]
Register wordlive's MCP server in an agent's config so a client can
drive your open document. It merges an mcpServers.<name> entry (default name
wordlive) that launches the stdio server with
uvx --from "wordlive[mcp,snapshot]" wordlive-mcp — no separate install step,
and the snapshot extra enables the vision tool. Offline: it only edits config,
never touches Word — restart the client to load the change.
--client claude-desktop(default) writes the OS-specificclaude_desktop_config.json;--client claude-codewrites a project-local./.mcp.json.--directory DIRregisters a local checkout viauv run --directory DIR wordlive-mcp(for development) instead of the PyPIuvxform.--config PATHtargets a specific config file;--printjust emits the JSON snippet (writing nothing) so you can paste it into any client.- It refuses to overwrite an existing server entry unless you pass
--force.
$ wordlive install-mcp --print
{
"mcpServers": {
"wordlive": {
"command": "uvx",
"args": ["--from", "wordlive[mcp,snapshot]", "wordlive-mcp"]
}
}
}
$ wordlive install-mcp
{"ok": true, "client": "claude-desktop", "path": ".../Claude/claude_desktop_config.json",
"server": "wordlive", "action": "created", "entry": {"command": "uvx", "args": ["--from", "wordlive[mcp,snapshot]", "wordlive-mcp"]}}
Failures: 1 if the config can't be read/written, isn't a JSON object, or the
server entry already exists without --force. For the bundle (.mcpb) and a
full tool reference, see the MCP server page.
LLM tool-use example¶
A typical agent loop looks like:
# 1. Discover what's addressable.
outline = json.loads(run(["wordlive", "outline"]))
# 2. Model picks an anchor and a new value, returns:
# {"anchor_id": "heading:3", "text": "Revised context section"}
# 3. Apply.
result = run(["wordlive", "replace",
"--anchor-id", anchor_id,
"--text", text])
# 4. Branch on exit code.
if result.returncode == 2: # anchor not found — re-fetch outline
...
elif result.returncode == 3: # Word busy — back off and retry
...
For multi-step intents, batch into one exec --script ops.json call instead.
See the Cookbook for full worked examples.