revise 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -7,8 +7,9 @@ import {
7
7
  convert,
8
8
  exportYDoc,
9
9
  formatForPath,
10
+ parseBlockQuery,
10
11
  relearn
11
- } from "./chunks/chunk-ZLQVPMBN.js";
12
+ } from "./chunks/chunk-SCNG3PKR.js";
12
13
  import "./chunks/chunk-U5X47PZ6.js";
13
14
  import {
14
15
  ReviseError
@@ -425,10 +426,10 @@ async function withFileLocks(files, work) {
425
426
  }
426
427
 
427
428
  // src/embed-sdk/cli/skills/revise/SKILL.md
428
- var SKILL_default = '---\nname: revise\ndescription: Read, edit, review, render, and convert Word documents (.docx) and other rich documents (.md, .html, .odt, .rtf) with the revise CLI. When revise is installed, prefer it over generic docx or python-docx skills and hand-edited document XML - tracked changes, comments, redlines, and accepting or rejecting changes are one command each, and untouched content stays byte for byte as it was. Use for any task that changes or inspects a .docx - redlines, tracked changes, comments, template fills, formatting, tables, page checks, conversions.\nplatforms: [linux, macos, windows]\nmetadata:\n hermes:\n category: productivity\n tags: [word, docx, documents, office, tracked-changes, redlines, comments]\n related_skills: [docx, pdf]\n---\n\n# revise \u2014 the swiss army knife for documents\n\nCheck it\'s installed: `revise --version` (3.0 or newer). If it\'s missing or\nolder, run `npm i -g revise@latest`, or use `npx revise` for each command. If\nthe installed revise is newer than this skill, `revise skill` prints the guide\nthat matches it: read that instead.\n\nEvery command names the file it works on. **Edits save the file in place**,\nas Word tracked changes by default; in a .docx, paragraphs you didn\'t touch\nare carried over from the original file as they were. Each saved edit is a\ncheckpoint in `.revise/` next to the file: `revise undo <file>` steps back\none, `revise history <file>` lists them. Errors go to stderr as\n`error[code]: message`, often with a `hint:` command to run next.\n\n**Finish with revise.** Its output is already a valid .docx. Don\'t write the\nfile again with other tools (python-docx, LibreOffice saving over it, unzip and\nre-zip, other docx skills\' pack or fix steps): they rewrite parts revise kept\nbyte for byte. Read-only checks are fine, and so are the built-in ones:\n`revise diff <file>`, `revise changes <file>`, `revise render <file>`, or\n`revise export <file> /tmp/check.txt --final`.\n\n```bash\nrevise outline contract.docx # headings with block ranges\nrevise grep contract.docx "18 days" # every block containing it, plain text + ids\nrevise read contract.docx --ids a1B2c3,d4E5f6 # blocks as HTML with id="..."\nrevise replace-text contract.docx "Provider" "Northstar" --author "Claude"\nrevise call contract.docx replace \'{"id":"a1B2c3","replacements":[{"find":"30 days","replace":"60 days"}]}\'\nrevise call contract.docx leave_comment \'{"id":"a1B2c3","anchor":{"text":"99.0%"},"comment":"Ask for 99.9%."}\'\nrevise render contract.docx --page 3 # page image (needs: npm i -g @napi-rs/canvas)\nrevise diff contract.docx # text effect of the pending tracked changes\n```\n\n- **Save as a new file?** Add `--out redlined.docx` to an edit\n (`replace-text`, `call`, `accept`, `reject`, `xml patch`): the original stays\n as it is, block ids you read from it work on the copy, and later edits go to the\n copy by its name. Or write a converted copy: `revise export contract.docx out.pdf`\n (.pdf, .md, .html, .txt, .docx; `--final` accepts all changes; a PDF\n always shows changes accepted). Existing files need `--force`.\n In a pipe: `cat x.md | revise convert - x.docx` (format recognized),\n `revise convert x.docx - --to md` (stdout; default md).\n- **Sign your changes:** `--author "Claude"` on each edit, or\n `export REVISE_AUTHOR="Claude"` once before the first one; it becomes the\n author on every tracked change and comment in Word.\n- **Tracked vs direct:** .docx edits are tracked changes;\n `revise mode <file> edit` applies edits directly, `--direct` for one call.\n- **Create a document in one command.** Write the content as HTML (the same\n HTML `read` shows) and run\n `revise new memo.docx --from draft.html --page letter --margins 1in --footer "Page {PAGE} of {PAGES}"`.\n In the HTML: `<p data-style-id="Title">\u2026</p>`, `<h1>`\u2013`<h3>`, `<ul>/<ol><li>`,\n `<table>` with `<th>`. Also `--orientation landscape`, `--header "\u2026"`,\n `--footer-right "{PAGE}"`. It prints what it wrote (pages, page setup,\n title, headings, lists, tables, footer): that is your check.\n- **Redline two versions:** `revise compare old.docx new.docx --out redline.docx`\n checks its text both ways (accepting all gives new\'s text, rejecting all\n gives old\'s; exit code 2 if not) and lists what it could not track\n (headers, page layout) \u2014 then review the redline with `changes`/`diff`.\n- **Accept or reject someone\'s tracked changes:** `revise changes <file>` summarizes them by\n section (`--section 8` or `--details` for each change\'s text). Then\n `revise reject <file> --section 8`, `revise accept <file> --except-section 8`,\n `revise accept <file> --all`; add `--dry-run` to see what would be accepted\n or rejected.\n- **Recipes** (no schema lookup needed):\n - Heading: `revise call f.docx style_blocks \'{"selectors":["#ID"],"attrs":[{"name":"heading","value":"1"}]}\'`\n - Title: `revise call f.docx replace_block \'{"id":"ID","content":["<p data-style-id=\\"Title\\">Text</p>"]}\'`\n - Page setup: `revise call f.docx set_page_layout \'{"pageSize":"a4","orientation":"portrait","margin":"2.5cm"}\'`\n - Footer: `revise call f.docx set_header_footer \'{"side":"footer","center":"Page {PAGE} of {PAGES}"}\'`\n - Table row: `revise call f.docx insert_table_row \'{"tableId":"T","referenceRowId":"R","position":"before","content":["a","b","c"]}\'`\n (T is the table\'s `id`, R a row\'s `<tr id>`, both from `read`)\n - Rewrite a block: `replace_block` takes `{"id":"\u2026","content":["<p>\u2026</p>"]}`; for\n small edits prefer `replace`, which tracks only the changed words.\n- `revise tools` lists every editing tool; `revise <tool>` alone (say,\n `revise insert_table_row`) prints the fields its JSON takes.\n- **JSON with quotes or apostrophes:** pass it on stdin instead of in quotes:\n ```bash\n revise call contract.docx replace - <<\'EOF\'\n {"id":"a1B2c3","replacements":[{"find":"Provider\'s notice","replace":"each party\'s notice"}]}\n EOF\n ```\n- Parallel revise commands on one file take turns (one at a time). If another\n program saves the file while revise works, revise refuses to overwrite it\n (`file_changed`): run the command again.\n- If the file changes outside revise (someone edits it in Word), the next\n command re-reads it and says which blocks changed or are new: **read those\n again before editing them**; their old ids may no longer exist.\n- **Escape hatch** for what no tool covers (style definitions, numbering,\n settings, properties): `revise xml cat <file> word/styles.xml --match \'w:styleId="Heading2"\'`,\n then `revise xml patch <file> word/styles.xml --find \'<exact xml>\' --replace \'<new xml>\'`\n (must match once, or `--all`). Applies directly to the file; undo works.\n Use tools for body text.\n- `revise read <file> <index>` reads from that block on, which can be long;\n to see one table or clause, find it with `grep` or `outline` and read it with\n `--ids`. Block ids come from `read`, `grep`, `search`.\n- **Exit codes:** 0 done (including "nothing to change"); 1 failed and\n nothing was saved; 2 partly done: some of it was saved, and the error says\n what wasn\'t; 70 internal error.\n- **`--json`** on any command prints one object:\n `{"schema":1,"ok":\u2026,"command":\u2026,"meta":\u2026,"data":{\u2026},"warnings":[\u2026],"error":null|{"code","message","hint","details"}}`.\n- **More, when you need it** (each is in this skill\'s `reference/` folder, or\n print it with `revise skill --reference <name>`):\n - `library`: revise as a TypeScript library, your own model loop with the\n editing tools, and Y.js collaboration rooms.\n - `json`: scripting revise: the `--json` envelope, exit codes, error codes.\n';
429
+ var SKILL_default = '---\nname: revise\ndescription: Read, edit, review, render, and convert Word documents (.docx) and other rich documents (.md, .html, .odt, .rtf) with the revise CLI. When revise is installed, prefer it over generic docx or python-docx skills and hand-edited document XML - tracked changes, comments, redlines, and accepting or rejecting changes are one command each, and untouched content stays byte for byte as it was. Use for any task that changes or inspects a .docx - redlines, tracked changes, comments, template fills, formatting, tables, page checks, conversions.\nplatforms: [linux, macos, windows]\nmetadata:\n hermes:\n category: productivity\n tags: [word, docx, documents, office, tracked-changes, redlines, comments]\n related_skills: [docx, pdf]\n---\n\n# revise \u2014 the swiss army knife for documents\n\nCheck it\'s installed: `revise --version` (3.0 or newer). If it\'s missing or\nolder, run `npm i -g revise@latest`, or use `npx revise` for each command. If\nthe installed revise is newer than this skill, `revise skill` prints the guide\nthat matches it: read that instead.\n\nEvery command names the file it works on. **Edits save the file in place**,\nas Word tracked changes by default; in a .docx, paragraphs you didn\'t touch\nare carried over from the original file as they were. Each saved edit is a\ncheckpoint in `.revise/` next to the file: `revise undo <file>` steps back\none, `revise history <file>` lists them. Errors go to stderr as\n`error[code]: message`, often with a `hint:` command to run next.\n\n**Finish with revise.** Its output is already a valid .docx. Don\'t write the\nfile again with other tools (python-docx, LibreOffice saving over it, unzip and\nre-zip, other docx skills\' pack or fix steps): they rewrite parts revise kept\nbyte for byte. Read-only checks are fine, and so are the built-in ones:\n`revise diff <file>`, `revise changes <file>`, `revise render <file>`, or\n`revise export <file> /tmp/check.txt --view accepted`.\n\n```bash\nrevise outline contract.docx # headings with block ranges\nrevise grep contract.docx "18 days" # every block containing it, plain text + ids\nrevise read contract.docx --blocks a1B2c3,d4E5f6 # blocks as HTML with id="..."\nrevise replace-all contract.docx "Provider" "Northstar" --author "Claude"\nrevise call contract.docx replace \'{"id":"a1B2c3","replacements":[{"find":"30 days","replace":"60 days"}]}\'\nrevise call contract.docx leave_comment \'{"id":"a1B2c3","anchor":{"text":"99.0%"},"comment":"Ask for 99.9%."}\'\nrevise render contract.docx --page 3 # page image (needs: npm i -g @napi-rs/canvas)\nrevise diff contract.docx # text effect of the pending tracked changes\n```\n\n- **Save as a new file?** Add `--out redlined.docx` to an edit\n (`replace-all`, `call`, `accept`, `reject`, `xml patch`): the original stays\n as it is, block ids you read from it work on the copy, and later edits go to the\n copy by its name. Or write a converted copy: `revise export contract.docx out.pdf`\n (.pdf, .md, .html, .txt, .docx; `--view accepted` accepts all changes,\n `--view original` rejects them; a PDF always shows changes accepted). Existing files need `--force`.\n In a pipe: `cat x.md | revise convert - x.docx` (format recognized),\n `revise convert x.docx - --to md` (stdout; default md).\n- **Sign your changes:** `--author "Claude"` on each edit, or\n `export REVISE_AUTHOR="Claude"` once before the first one; it becomes the\n author on every tracked change and comment in Word.\n- **Tracked vs direct:** .docx edits are tracked changes;\n `revise mode <file> edit` applies edits directly, `--direct` for one call.\n- **Create a document in one command.** Write the content as HTML (the same\n HTML `read` shows) and run\n `revise new memo.docx --content draft.html --page-size letter --margins 1in --footer "Page {PAGE} of {PAGES}"`.\n In the HTML: `<p data-style-id="Title">\u2026</p>`, `<h1>`\u2013`<h3>`, `<ul>/<ol><li>`,\n `<table>` with `<th>`. Also `--orientation landscape`, `--header "\u2026"`,\n `--footer-right "{PAGE}"`. It prints what it wrote (pages, page setup,\n title, headings, lists, tables, footer): that is your check.\n- **Redline two versions:** `revise compare old.docx new.docx --out redline.docx`\n checks its text both ways (accepting all gives new\'s text, rejecting all\n gives old\'s; exit code 2 if not) and lists what it could not track\n (headers, page layout) \u2014 then review the redline with `changes`/`diff`.\n- **Read a redline:** `read` shows pending changes as `<del>`/`<ins>` with\n `data-author`, and `grep` finds deleted text too (`[-old-]{+new+}`). Add\n `--view accepted` or `--view original` for the text with every change\n accepted or rejected. Pasting tracked HTML into a tool edits the accepted text.\n- **Accept or reject someone\'s tracked changes:** `revise changes <file>` summarizes them by\n section (`--section 8` or `--details` for each change\'s text). Then\n `revise reject <file> --section 8` (8.1, 8.2\u2026 included),\n `revise accept <file> --except-section 8`, `--blocks <id>..<id>` for a\n range of blocks, or `revise accept <file> --all`; add `--dry-run` to see what would be accepted\n or rejected.\n- **Recipes** (no schema lookup needed):\n - Heading: `revise call f.docx style_blocks \'{"selectors":["#ID"],"attrs":[{"name":"heading","value":"1"}]}\'`\n - Title: `revise call f.docx replace_block \'{"id":"ID","content":["<p data-style-id=\\"Title\\">Text</p>"]}\'`\n - Page setup: `revise call f.docx set_page_layout \'{"pageSize":"a4","orientation":"portrait","margin":"2.5cm"}\'`\n - Footer: `revise call f.docx set_header_footer \'{"side":"footer","center":"Page {PAGE} of {PAGES}"}\'`\n - Table row: `revise call f.docx insert_table_row \'{"tableId":"T","referenceRowId":"R","position":"before","content":["a","b","c"]}\'`\n (T is the table\'s `id`, R a row\'s `<tr id>`, both from `read`)\n - Rewrite a block: `replace_block` takes `{"id":"\u2026","content":["<p>\u2026</p>"]}`; for\n small edits prefer `replace`, which tracks only the changed words.\n- `revise tools` lists every editing tool; `revise <tool>` alone (say,\n `revise insert_table_row`) prints the fields its JSON takes.\n- **JSON with quotes or apostrophes:** pass it on stdin instead of in quotes:\n ```bash\n revise call contract.docx replace - <<\'EOF\'\n {"id":"a1B2c3","replacements":[{"find":"Provider\'s notice","replace":"each party\'s notice"}]}\n EOF\n ```\n- Parallel revise commands on one file take turns (one at a time). If another\n program saves the file while revise works, revise refuses to overwrite it\n (`file_changed`): run the command again.\n- If the file changes outside revise (someone edits it in Word), the next\n command re-reads it and says which blocks changed or are new: **read those\n again before editing them**; their old ids may no longer exist.\n- **Escape hatch** for what no tool covers (style definitions, numbering,\n settings, properties): `revise xml cat <file> word/styles.xml --match \'w:styleId="Heading2"\'`,\n then `revise xml patch <file> word/styles.xml --find \'<exact xml>\' --replace \'<new xml>\'`\n (must match once, or `--all`). Applies directly to the file; undo works.\n Use tools for body text.\n- `revise read <file> <index>` reads from that block on, which can be long;\n to see one table or clause, find it with `grep` or `outline` and read it with\n `--blocks`. Block ids come from `read`, `grep`, `search`.\n- **Exit codes:** 0 done (including "nothing to change"); 1 failed and\n nothing was saved; 2 partly done: some of it was saved, and the error says\n what wasn\'t; 70 internal error.\n- **`--json`** on any command prints one object:\n `{"schema":1,"ok":\u2026,"command":\u2026,"meta":\u2026,"data":{\u2026},"warnings":[\u2026],"error":null|{"code","message","hint","details"}}`.\n- **More, when you need it** (each is in this skill\'s `reference/` folder, or\n print it with `revise skill --reference <name>`):\n - `library`: revise as a TypeScript library, your own model loop with the\n editing tools, and Y.js collaboration rooms.\n - `json`: scripting revise: the `--json` envelope, exit codes, error codes.\n';
429
430
 
430
431
  // src/embed-sdk/cli/skills/revise/reference/library.md
431
- var library_default = '# revise as a TypeScript library\n\nRead this when the work is code, not shell commands: a Node script or service\nthat edits documents, your own model loop with Revise\'s editing tools, or a\ncollaboration server. The CLI is a shell over this API. ESM only, Node 20+.\nFull docs: https://revise.io/developer/docs/revise/library.md\n\n```ts\nimport { writeFile } from "node:fs/promises";\nimport { ReviseDocument } from "revise";\n\nconst doc = await ReviseDocument.open("contract.docx", { author: "Legal bot" });\nawait doc.replaceText("Provider", "Northstar"); // tracked changes\nawait doc.reject({ section: "8" });\nconst { bytes, fidelity } = await doc.export({ format: "docx" });\nawait writeFile("contract-out.docx", bytes);\ndoc.dispose();\n```\n\n## Open\n\n- `ReviseDocument.open(source, options?)`: a path, or `{ bytes, filename }`\n (.docx, .odt, .rtf, .md, .html, .txt). A Word file keeps its package, so\n untouched content exports byte for byte.\n- `ReviseDocument.create({ html?, markdown?, title? })`, `ReviseDocument.restore(snapshot)`,\n `ReviseDocument.fromYDoc(ydoc)` (a Y.Doc you own, such as a live room).\n- Options: `author`, `mode` (`"suggesting"`: tracked changes, the default for\n Word files; `"editing"`: direct), `documentId`, `images`.\n- Calls run one at a time in order; call `dispose()` when done.\n\n## Read\n\n`status()`, `outline()`, `summary()`, `grep(text, { ignoreCase })` \u2192\n`[{ id, blockIndex, count, text }]`.\n\n## Edit\n\n- `replaceText(find, replace, { ids? })` \u2192 `{ replaced, blocks, changeIds, failures }`.\n- Everything else is an editing tool: `doc.tools.definitions()` lists them with\n JSON schemas; `doc.tools.call(name, input)` runs one with typed input;\n `doc.tools.run(name, input)` with untyped input, as a model sends it. Neither\n throws for a bad edit: `{ ok: true, value }` or `{ ok: false, error }`.\n- `await doc.setMode("editing" | "suggesting")`.\n\n## Review\n\n`changes({ section? })`, `preview(selection)`, `accept(selection)`,\n`reject(selection)` \u2192 `{ resolved, missing, unresolved }`, `diff()`. A selection\nis exactly one of `ids`, `all`, or `section` / `exceptSection` (`"8"`, `"8."`,\nor the heading text).\n\n## Output\n\n`export({ format, mode? })` with `docx`, `pdf`, `markdown`, `html`, `txt`;\n`mode: "final"` accepts everything. Returns `bytes`, `mode`,\n`pendingTrackedChanges`, `fidelity` (.docx: `"graft"` kept untouched content\nbyte for byte, `"regenerated"` wrote it fresh), `warnings`.\n`renderPages({ startPage, pageCount, view })` \u2192 one PNG (needs `@napi-rs/canvas`).\n`compare(a, b)` \u2192 a redline document; `convert(source, format)` \u2192 bytes.\nRaw package: `listParts`, `catPart`, `patchPackage`.\n\n## Your own model loop\n\n`agentTools(doc, { provider })` returns the tools in the shape a model API\nexpects and a `handle` that runs one call and returns the result message.\nProviders: `"anthropic"` (Messages `tool_use` \u2192 `tool_result`), `"openai"` (Chat\nCompletions `tool_calls` \u2192 `role: "tool"`), `"openai-responses"`\n(`function_call` \u2192 `function_call_output`). No model SDK is a dependency.\n\n```ts\nimport Anthropic from "@anthropic-ai/sdk";\nimport { ReviseDocument, agentTools } from "revise";\n\nconst anthropic = new Anthropic();\nconst doc = await ReviseDocument.open("contract.docx", {\n author: "Contract bot",\n});\nconst agent = agentTools(doc, { provider: "anthropic" });\nconst messages: Anthropic.MessageParam[] = [\n { role: "user", content: "Change the governing law to New York." },\n];\nfor (;;) {\n const reply = await anthropic.messages.create({\n model: "claude-sonnet-5-5",\n max_tokens: 4096,\n tools: agent.tools,\n messages,\n });\n messages.push({ role: "assistant", content: reply.content });\n const calls = reply.content.filter((block) => block.type === "tool_use");\n if (calls.length === 0) break;\n messages.push({\n role: "user",\n content: await Promise.all(calls.map(agent.handle)),\n });\n}\n```\n\n`handle` never throws for a bad call; the model gets the error and corrects\nitself. Options: `include`, `exclude`, `maxResultChars`. After compacting the\ntranscript, call `agent.reset()`. Docs: https://revise.io/developer/docs/revise/agents.md\n\n## Collaboration rooms\n\nEvery Revise document is a Y.js document. On a sync server:\n`seedYDoc(room, "contract.docx")` seeds an empty room (safe on every\nconnection); `ReviseDocument.fromYDoc(room, { author })` edits a live room as\ntracked changes that appear in every open `@reviseio/editor`; `export()` downloads\nit. Dispose the ReviseDocument, not the room. Docs:\nhttps://revise.io/developer/docs/revise/collaboration.md\n\n## Errors\n\nExpected failures (missing file, unsupported format, malformed patch) throw\n`ReviseError` with a stable `code`. Tool calls return errors as values.\n';
432
+ var library_default = '# revise as a TypeScript library\n\nRead this when the work is code, not shell commands: a Node script or service\nthat edits documents, your own model loop with Revise\'s editing tools, or a\ncollaboration server. The CLI is a shell over this API. ESM only, Node 20+.\nFull docs: https://revise.io/developer/docs/revise/library.md\n\n```ts\nimport { writeFile } from "node:fs/promises";\nimport { ReviseDocument } from "revise";\n\nconst doc = await ReviseDocument.open("contract.docx", { author: "Legal bot" });\nawait doc.replaceAll("Provider", "Northstar"); // tracked changes\nawait doc.reject({ section: "8" });\nconst { bytes, fidelity } = await doc.export({ format: "docx" });\nawait writeFile("contract-out.docx", bytes);\ndoc.dispose();\n```\n\n## Open\n\n- `ReviseDocument.open(source, options?)`: a path, or `{ bytes, filename }`\n (.docx, .odt, .rtf, .md, .html, .txt). A Word file keeps its package, so\n untouched content exports byte for byte.\n- `ReviseDocument.create({ html?, markdown?, title? })`, `ReviseDocument.restore(snapshot)`,\n `ReviseDocument.fromYDoc(ydoc)` (a Y.Doc you own, such as a live room).\n- Options: `author`, `mode` (`"suggesting"`: tracked changes, the default for\n Word files; `"editing"`: direct), `documentId`, `images`.\n- Calls run one at a time in order; call `dispose()` when done.\n\n## Read\n\n`status()`, `outline()`, `summary()`, `grep(text, { ignoreCase })` \u2192\n`[{ id, blockIndex, count, text }]`.\n\n## Edit\n\n- `replaceAll(find, replace, { ids? })` \u2192 `{ replaced, blocks, changeIds, failures }`.\n- Everything else is an editing tool: `doc.tools.definitions()` lists them with\n JSON schemas; `doc.tools.call(name, input)` runs one with typed input;\n `doc.tools.run(name, input)` with untyped input, as a model sends it. Neither\n throws for a bad edit: `{ ok: true, value }` or `{ ok: false, error }`.\n- `await doc.setMode("editing" | "suggesting")`.\n\n## Review\n\n`changes({ section? })`, `preview(selection)`, `accept(selection)`,\n`reject(selection)` \u2192 `{ resolved, missing, unresolved }`, `diff()`. A selection\nis exactly one of `ids`, `all`, or `section` / `exceptSection` (`"8"`, `"8."`,\nor the heading text).\n\n## Output\n\n`export({ format, view? })` with `docx`, `pdf`, `markdown`, `html`, `txt`;\n`view: "accepted"` accepts everything, `"original"` rejects it. Returns `bytes`, `mode`, `view`,\n`pendingTrackedChanges`, `fidelity` (.docx: `"graft"` kept untouched content\nbyte for byte, `"regenerated"` wrote it fresh), `warnings`.\n`renderPages({ startPage, pageCount, view })` \u2192 one PNG (needs `@napi-rs/canvas`).\n`compare(a, b)` \u2192 a redline document; `convert(source, format)` \u2192 bytes.\nRaw package: `listParts`, `catPart`, `patchPackage`.\n\n## Your own model loop\n\n`agentTools(doc, { provider })` returns the tools in the shape a model API\nexpects and a `handle` that runs one call and returns the result message.\nProviders: `"anthropic"` (Messages `tool_use` \u2192 `tool_result`), `"openai"` (Chat\nCompletions `tool_calls` \u2192 `role: "tool"`), `"openai-responses"`\n(`function_call` \u2192 `function_call_output`). No model SDK is a dependency.\n\n```ts\nimport Anthropic from "@anthropic-ai/sdk";\nimport { ReviseDocument, agentTools } from "revise";\n\nconst anthropic = new Anthropic();\nconst doc = await ReviseDocument.open("contract.docx", {\n author: "Contract bot",\n});\nconst agent = agentTools(doc, { provider: "anthropic" });\nconst messages: Anthropic.MessageParam[] = [\n { role: "user", content: "Change the governing law to New York." },\n];\nfor (;;) {\n const reply = await anthropic.messages.create({\n model: "claude-sonnet-5-5",\n max_tokens: 4096,\n tools: agent.tools,\n messages,\n });\n messages.push({ role: "assistant", content: reply.content });\n const calls = reply.content.filter((block) => block.type === "tool_use");\n if (calls.length === 0) break;\n messages.push({\n role: "user",\n content: await Promise.all(calls.map(agent.handle)),\n });\n}\n```\n\n`handle` never throws for a bad call; the model gets the error and corrects\nitself. Options: `include`, `exclude`, `maxResultChars`. After compacting the\ntranscript, call `agent.reset()`. Docs: https://revise.io/developer/docs/revise/agents.md\n\n## Collaboration rooms\n\nEvery Revise document is a Y.js document. On a sync server:\n`seedYDoc(room, "contract.docx")` seeds an empty room (safe on every\nconnection); `ReviseDocument.fromYDoc(room, { author })` edits a live room as\ntracked changes that appear in every open `@reviseio/editor`; `export()` downloads\nit. Dispose the ReviseDocument, not the room. Docs:\nhttps://revise.io/developer/docs/revise/collaboration.md\n\n## Errors\n\nExpected failures (missing file, unsupported format, malformed patch) throw\n`ReviseError` with a stable `code`. Tool calls return errors as values.\n';
432
433
 
433
434
  // src/embed-sdk/cli/skills/revise/reference/json.md
434
435
  var json_default = '# Scripting revise: --json, exit codes, error codes\n\nRead this when a script or program runs revise and needs to branch on the\nresult. Full contract: https://revise.io/developer/docs/revise/json.md (also\nshipped as a JSON Schema: `revise/cli-output.schema.json`).\n\n## The envelope\n\nWith `--json`, every command prints exactly one JSON object on stdout and\nnothing on stderr:\n\n```json\n{"schema":1,"ok":true,"command":"status","meta":{"version":"3.0.0","build":"\u2026"},\n "data":{\u2026},"warnings":[],"error":null}\n```\n\n- `ok` is true exactly when `error` is null. `data` can be present on failure\n (what was saved before the failure) and null on success with nothing to say.\n- `command` is the canonical name (`xml patch`, `skill install`; `call` for\n `revise <tool> <file>`; `help` for any `--help`).\n- Within schema 1, fields are only added. Ignore unknown fields; handle unknown\n codes generically.\n- Inside `data`: camelCase keys, counts end in `Count`, paths are absolute,\n n/a values are `null`, empty lists are `[]`.\n- Writing a document to stdout (`revise convert x.docx -`) can\'t combine with\n `--json`.\n\n## Exit codes\n\n| Code | Meaning |\n| ---- | --------------------------------------------------------------------------------------------- |\n| 0 | Done, including "nothing to change". |\n| 1 | Failed, nothing saved. Human mode prints nothing on stdout. |\n| 2 | Partly done: `data` says what happened, `error` what didn\'t. |\n| 70 | Internal error, or `save_uncertain`: check the file; the next command on it settles the save. |\n\n## Errors and warnings\n\n`error`: `code` (stable, branch on it), `message` (for people and models;\nwording may change), `hint` (a shell command to run next, or null),\n`details` (structured, or null). Warnings have the same shape minus `hint`.\n\nCommon error codes: `usage`, `unknown_command`, `unknown_tool`,\n`file_not_found`, `unsupported_format`, `import_failed`, `would_overwrite`\n(add `--force`), `same_file`, `file_busy`, `file_changed` (another program saved\nit; run again), `tool_error` (a tool refused: `details.toolCode`),\n`unknown_search_result` / `stale_search_result` (search again),\n`change_not_found`, `find_not_found` / `find_ambiguous` (xml patch),\n`body_shape_changed` (use the tools for body text), `save_uncertain`,\n`internal`.\n\nWarnings worth acting on: `file_changed_outside` (re-read the blocks in\n`details.changedBlockIds` before editing them), `changes_accepted` (the output\nformat can\'t hold tracked changes), `font_fallback`, `untracked_regions`\n(compare).\n\n## Saving\n\nCommands that change a document report `file`, `saved` (did this command\nwrite it), and `checkpoint` (where the file is now; `revise undo <file>` steps\nback). Commands that write another file report `output` and `outputWritten`.\nWith `--out`, edits report `copiedFrom` (the original) and `file` (the copy).\nTool calls also report `changeIds` (tracked changes created), `html` (blocks\nreturned, with ids), and `result` (the tool\'s own result; snake_case keys like\n`search_result_id`).\n';
@@ -579,11 +580,16 @@ function utf8Of(bytes) {
579
580
  return bytes;
580
581
  } catch {
581
582
  if (bytes.includes(0)) return bytes;
582
- return new TextEncoder().encode(
583
- new TextDecoder("windows-1252").decode(bytes)
584
- );
583
+ return new TextEncoder().encode(windows1252(bytes));
585
584
  }
586
585
  }
586
+ var WINDOWS_1252_C1 = "\u20AC\x81\u201A\u0192\u201E\u2026\u2020\u2021\u02C6\u2030\u0160\u2039\u0152\x8D\u017D\x8F\x90\u2018\u2019\u201C\u201D\u2022\u2013\u2014\u02DC\u2122\u0161\u203A\u0153\x9D\u017E\u0178";
587
+ function windows1252(bytes) {
588
+ return new TextDecoder("windows-1252").decode(bytes).replace(
589
+ /[\u0080-\u009f]/g,
590
+ (char) => WINDOWS_1252_C1[char.charCodeAt(0) - 128]
591
+ );
592
+ }
587
593
  function utf16Of(bytes) {
588
594
  if (bytes[0] === 255 && bytes[1] === 254) return "utf-16le";
589
595
  if (bytes[0] === 254 && bytes[1] === 255) return "utf-16be";
@@ -657,6 +663,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
657
663
  "dry-run",
658
664
  "regex",
659
665
  "ignore-case",
666
+ "exact",
660
667
  "json",
661
668
  "all",
662
669
  "final",
@@ -670,6 +677,10 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
670
677
  ]);
671
678
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
672
679
  "author",
680
+ "blocks",
681
+ "content",
682
+ "page-size",
683
+ "results-page",
673
684
  "dir",
674
685
  "except-section",
675
686
  "file",
@@ -734,6 +745,33 @@ function flagString(args, name) {
734
745
  const value = args.flags[name];
735
746
  return typeof value === "string" ? value : void 0;
736
747
  }
748
+ function blockIdsFlag(args, command) {
749
+ const value = flagString(args, "blocks") ?? flagString(args, "ids");
750
+ if (value === void 0) return void 0;
751
+ const blocks = parseBlockQuery(value);
752
+ if (blocks.some((b) => typeof b !== "string")) {
753
+ throw fail(
754
+ `revise ${command} takes block ids, not ranges.`,
755
+ "usage",
756
+ `revise ${command} --help`
757
+ );
758
+ }
759
+ return blocks;
760
+ }
761
+ var VIEWS = ["tracked", "accepted", "original"];
762
+ function viewFlag(args, fallback) {
763
+ const view = flagString(args, "view");
764
+ if (view !== void 0 && !VIEWS.includes(view)) {
765
+ throw fail("--view must be tracked, accepted, or original.", "usage");
766
+ }
767
+ if (args.flags.final === true) {
768
+ if (view !== void 0 && view !== "accepted") {
769
+ throw fail(`--final means --view accepted, not --view ${view}.`, "usage");
770
+ }
771
+ return "accepted";
772
+ }
773
+ return view ?? fallback;
774
+ }
737
775
  function flagInt(args, name) {
738
776
  const value = flagString(args, name);
739
777
  if (value === void 0) return void 0;
@@ -832,8 +870,8 @@ var ui = PLAIN;
832
870
  var uiErr = PLAIN;
833
871
  function producer() {
834
872
  return {
835
- version: true ? "3.0.0" : "dev",
836
- build: true ? "a60344a6f" : null
873
+ version: true ? "3.1.0" : "dev",
874
+ build: true ? "7cded52c5" : null
837
875
  };
838
876
  }
839
877
  var warnings = [];
@@ -1244,7 +1282,7 @@ async function restoreReadContext(loaded, input) {
1244
1282
  }
1245
1283
  }
1246
1284
  function cliVocabulary(message, file) {
1247
- return message.replace(/read_blocks_from_index\(index=(\d+)\)/g, `revise read ${file} $1`).replace(/read_specific_blocks\b/g, `revise read ${file} --ids`).replace(/search_document\b/g, `revise search ${file}`);
1285
+ return message.replace(/read_blocks_from_index\(index=(\d+)\)/g, `revise read ${file} $1`).replace(/read_specific_blocks\b/g, `revise read ${file} --blocks`).replace(/search_document\b/g, `revise search ${file}`);
1248
1286
  }
1249
1287
  function toolData(loaded, tool, fields) {
1250
1288
  return { file: loaded.meta.file, tool, ...fields };
@@ -1304,7 +1342,7 @@ async function runTool(loaded, name, input, options = {}) {
1304
1342
  const { result, checkpoint } = await mutate(
1305
1343
  loaded,
1306
1344
  name,
1307
- () => loaded.doc.tools.run(name, input, options),
1345
+ () => loaded.doc.tools.run(name, input, { direct: options.direct }),
1308
1346
  (r) => "error" in r ? r.error.message : r.value.message ?? name,
1309
1347
  { saveIf: (r) => !("error" in r) }
1310
1348
  );
@@ -1355,16 +1393,19 @@ async function runTool(loaded, name, input, options = {}) {
1355
1393
  value = persistSearch(loaded.meta, input, value);
1356
1394
  if (value.context?.html) rememberReadBlocks(loaded.meta, value.context.html);
1357
1395
  const changeIds = value.suggestionIds ?? [];
1358
- const html = value.context?.html ?? null;
1396
+ const html = value.context?.html && options.view && options.view !== "accepted" ? await loaded.doc.viewHtml(value.context.html, options.view) : value.context?.html ?? null;
1359
1397
  return {
1360
- data: toolData(loaded, name, {
1361
- saved: checkpoint !== null,
1362
- checkpoint: now,
1363
- message: value.message,
1364
- changeIds,
1365
- html,
1366
- result: value.data ?? null
1367
- }),
1398
+ data: {
1399
+ ...toolData(loaded, name, {
1400
+ saved: checkpoint !== null,
1401
+ checkpoint: now,
1402
+ message: value.message,
1403
+ changeIds,
1404
+ html,
1405
+ result: value.data ?? null
1406
+ }),
1407
+ ...options.view ? { view: options.view } : {}
1408
+ },
1368
1409
  text: () => {
1369
1410
  const lines = [];
1370
1411
  if (value.message)
@@ -1450,7 +1491,7 @@ async function cmdNew(args) {
1450
1491
  if (requestedMode && !["suggest", "edit"].includes(requestedMode)) {
1451
1492
  throw fail("--mode must be suggest or edit.", "usage", "revise new --help");
1452
1493
  }
1453
- const from = flagString(args, "from");
1494
+ const from = flagString(args, "content") ?? flagString(args, "from");
1454
1495
  if (from !== void 0 && !fs2.existsSync(from)) {
1455
1496
  throw fail(`No such file: ${from}`, "file_not_found");
1456
1497
  }
@@ -1505,11 +1546,11 @@ function newDocumentSteps(args, probe) {
1505
1546
  const layout = probe.tools.definitions().find((tool) => tool.name === "set_page_layout");
1506
1547
  const allowed = (property) => layout?.inputSchema.properties?.[property]?.enum ?? [];
1507
1548
  const layoutInput = {};
1508
- const page = flagString(args, "page")?.toLowerCase();
1549
+ const page = (flagString(args, "page-size") ?? flagString(args, "page"))?.toLowerCase();
1509
1550
  if (page !== void 0) {
1510
1551
  if (!allowed("pageSize").includes(page)) {
1511
1552
  throw fail(
1512
- `--page must be one of: ${allowed("pageSize").join(", ")}.`,
1553
+ `--page-size must be one of: ${allowed("pageSize").join(", ")}.`,
1513
1554
  "usage",
1514
1555
  "revise new --help"
1515
1556
  );
@@ -1652,22 +1693,111 @@ async function cmdStatus(args) {
1652
1693
  }
1653
1694
  async function cmdRead(args) {
1654
1695
  const loaded = await loadDocument(args);
1655
- const ids = flagString(args, "ids");
1696
+ const ids = blockIdsFlag(args, "read");
1697
+ const view = viewFlag(args, "tracked");
1656
1698
  if (ids) {
1657
- return runTool(loaded, "read_specific_blocks", {
1658
- block_ids: ids.split(",").map((id) => id.trim()).filter(Boolean)
1659
- });
1699
+ return runTool(
1700
+ loaded,
1701
+ "read_specific_blocks",
1702
+ { block_ids: ids },
1703
+ { view }
1704
+ );
1660
1705
  }
1661
1706
  const index = strictInt(args.positional[0] ?? "0", "The block index");
1662
- return runTool(loaded, "read_blocks_from_index", { index });
1707
+ return runTool(loaded, "read_blocks_from_index", { index }, { view });
1663
1708
  }
1664
1709
  async function cmdSearch(args) {
1665
1710
  if (!args.positional.length) throw usageError("search");
1711
+ const view = viewFlag(args, "tracked");
1666
1712
  const loaded = await loadDocument(args);
1667
- return runTool(loaded, "search_document", {
1668
- queries: args.positional,
1669
- page: flagInt(args, "page") ?? 1
1713
+ const textMatches = (only) => args.positional.flatMap((query) => {
1714
+ const matcher = query.startsWith("regex:") ? safeRegExp(query.slice(6)) : query;
1715
+ return loaded.doc.grep(matcher, { ignoreCase: true, view: "original" }).filter(
1716
+ (hit) => only ? hit.matches.some((m) => m.change === only) : true
1717
+ ).map((hit) => ({
1718
+ query,
1719
+ blockId: hit.id,
1720
+ blockIndex: hit.blockIndex,
1721
+ text: hit.text,
1722
+ changeIds: [
1723
+ ...new Set(
1724
+ hit.matches.filter((m) => m.change === "deleted").map((m) => m.changeId ?? "")
1725
+ )
1726
+ ].filter(Boolean)
1727
+ }));
1670
1728
  });
1729
+ const viewLines = (matches, heading) => matches.length ? [
1730
+ "",
1731
+ heading,
1732
+ ...matches.map(
1733
+ (m) => `${ui.cyan(m.blockId)} ${highlight(m.text, m.query.startsWith("regex:") ? safeRegExp(m.query.slice(6)) : m.query, true)}`
1734
+ )
1735
+ ] : [];
1736
+ if (view === "original") {
1737
+ const matches = textMatches(null);
1738
+ rememberReadBlocks(
1739
+ loaded.meta,
1740
+ matches.map((m) => `<p id="${m.blockId}">`).join("")
1741
+ );
1742
+ return {
1743
+ data: {
1744
+ ...toolData(loaded, "search_document", {
1745
+ saved: false,
1746
+ checkpoint: currentCheckpoint(loaded.meta),
1747
+ message: null,
1748
+ changeIds: [],
1749
+ html: null,
1750
+ result: null
1751
+ }),
1752
+ view,
1753
+ viewMatches: matches
1754
+ },
1755
+ text: () => matches.length ? [
1756
+ ...viewLines(
1757
+ matches,
1758
+ `${plural(matches.length, "block")} match in the original text (before the pending changes):`
1759
+ ).slice(1),
1760
+ "",
1761
+ ui.dim(
1762
+ "No search_result_id: it names blocks of the current text. Search without --view original for one."
1763
+ )
1764
+ ].join("\n") : `No matches in the original text for ${args.positional.map((q) => `"${q}"`).join(", ")}.`
1765
+ };
1766
+ }
1767
+ const outcome = await runTool(
1768
+ loaded,
1769
+ "search_document",
1770
+ {
1771
+ queries: args.positional,
1772
+ // --page is the 3.0 name.
1773
+ page: flagInt(args, "results-page") ?? flagInt(args, "page") ?? 1
1774
+ },
1775
+ { view }
1776
+ );
1777
+ if (view !== "tracked" || outcome.error) return outcome;
1778
+ const deleted = textMatches("deleted");
1779
+ rememberReadBlocks(
1780
+ loaded.meta,
1781
+ deleted.map((m) => `<p id="${m.blockId}">`).join("")
1782
+ );
1783
+ return {
1784
+ ...outcome,
1785
+ data: { ...outcome.data, viewMatches: deleted },
1786
+ text: () => [
1787
+ outcome.text(),
1788
+ ...viewLines(
1789
+ deleted,
1790
+ `Also in deleted text (pending changes; ${ui.dim("[-deleted-]{+inserted+}")}):`
1791
+ )
1792
+ ].join("\n")
1793
+ };
1794
+ }
1795
+ function safeRegExp(pattern) {
1796
+ try {
1797
+ return new RegExp(pattern, "gi");
1798
+ } catch {
1799
+ return pattern;
1800
+ }
1671
1801
  }
1672
1802
  async function cmdOutline(args) {
1673
1803
  const loaded = await loadDocument(args);
@@ -1802,8 +1932,11 @@ var BEFORE_FIRST_HEADING = "(before the first heading)";
1802
1932
  async function cmdChanges(args) {
1803
1933
  const loaded = await loadDocument(args);
1804
1934
  const section = flagString(args, "section");
1805
- const changes = await loaded.doc.changes({ section });
1806
- const details = args.flags.details === true || Boolean(section);
1935
+ const blocksFlag = flagString(args, "blocks");
1936
+ const blocks = blocksFlag ? parseBlockQuery(blocksFlag) : void 0;
1937
+ const changes = await loaded.doc.changes({ section, blocks });
1938
+ const filtered = Boolean(section || blocks);
1939
+ const details = args.flags.details === true || filtered;
1807
1940
  const entries = changes.map((c) => ({
1808
1941
  changeId: c.id,
1809
1942
  type: c.type,
@@ -1819,17 +1952,18 @@ async function cmdChanges(args) {
1819
1952
  `revise accept ${loaded.shown} <change-id...>`,
1820
1953
  `revise reject ${loaded.shown} <change-id...>`
1821
1954
  ) + `
1822
- ${ui.dim(` (or --all, --section <number or title>; add --dry-run to preview)`)}`;
1955
+ ${ui.dim(` (or --all, --section <number or title>, --blocks <id,id or first..last>; add --dry-run to preview)`)}`;
1823
1956
  return {
1824
1957
  data: {
1825
1958
  file: loaded.meta.file,
1826
1959
  section: section ?? null,
1960
+ blocks: blocksFlag ?? null,
1827
1961
  trackedChangeCount: entries.length,
1828
1962
  changes: entries
1829
1963
  },
1830
1964
  text: () => {
1831
1965
  if (!entries.length) {
1832
- return section ? `No tracked changes in section "${section}".` : "No tracked changes.";
1966
+ return section ? `No tracked changes in section "${section}"${blocks ? " and those blocks" : ""}.` : blocks ? "No tracked changes in those blocks." : "No tracked changes.";
1833
1967
  }
1834
1968
  if (!details && entries.length > 30) {
1835
1969
  return [
@@ -1880,10 +2014,18 @@ async function cmdSettle(args, accept) {
1880
2014
  const all = args.flags.all === true;
1881
2015
  const section = flagString(args, "section");
1882
2016
  const exceptSection = flagString(args, "except-section");
1883
- if (!all && !section && !exceptSection && !args.positional.length) {
2017
+ const blocksFlag = flagString(args, "blocks");
2018
+ const blocks = blocksFlag ? parseBlockQuery(blocksFlag) : void 0;
2019
+ if (!all && !section && !exceptSection && !blocks && !args.positional.length) {
1884
2020
  throw usageError(action);
1885
2021
  }
1886
- const selection = { ids: args.positional, all, section, exceptSection };
2022
+ const selection = {
2023
+ ids: args.positional,
2024
+ all,
2025
+ section,
2026
+ exceptSection,
2027
+ blocks
2028
+ };
1887
2029
  const base = {
1888
2030
  file: loaded.meta.file,
1889
2031
  action
@@ -1950,7 +2092,7 @@ async function cmdSettle(args, accept) {
1950
2092
  const done = accept ? "Accepted" : "Rejected";
1951
2093
  const failedOutright = !result.resolved.length && (result.missing.length || failed.length);
1952
2094
  const text = () => failedOutright ? "" : [
1953
- result.resolved.length ? `${ui.green(done)} ${plural(result.resolved.length, "tracked change")}.` : section || exceptSection ? "No tracked changes matched." : "No tracked changes to " + action + ".",
2095
+ result.resolved.length ? `${ui.green(done)} ${plural(result.resolved.length, "tracked change")}.` : section || exceptSection || blocks ? "No tracked changes matched." : "No tracked changes to " + action + ".",
1954
2096
  ...checkpoint !== null ? [savedLine(loaded, checkpoint)] : []
1955
2097
  ].join("\n");
1956
2098
  if (result.missing.length || failed.length) {
@@ -2149,7 +2291,7 @@ async function cmdExport(args) {
2149
2291
  assertCanOverwrite(args, out2);
2150
2292
  const result = await loaded.doc.export({
2151
2293
  format,
2152
- mode: args.flags.final === true ? "final" : "review"
2294
+ view: viewFlag(args, "tracked")
2153
2295
  });
2154
2296
  writeOutput(args, out2, result.bytes);
2155
2297
  let summary = null;
@@ -2189,15 +2331,16 @@ async function cmdExport(args) {
2189
2331
  format: formatName(format),
2190
2332
  byteCount: result.bytes.length,
2191
2333
  final: result.mode === "final",
2334
+ view: result.view,
2192
2335
  trackedChangeCount,
2193
2336
  docxExport,
2194
2337
  summary
2195
2338
  },
2196
2339
  text: () => [
2197
- `${ui.green("Wrote")} ${shellArg(target)} (${sizeText(result.bytes.length)}${result.mode === "final" ? ", tracked changes accepted" : ""}).`,
2340
+ `${ui.green("Wrote")} ${shellArg(target)} (${sizeText(result.bytes.length)}${result.view === "original" ? ", as it was before the tracked changes" : result.mode === "final" ? ", tracked changes accepted" : ""}).`,
2198
2341
  ...summary ? [summaryText(summary)] : [],
2199
2342
  ...format === "docx" && trackedChangeCount ? [
2200
- `Keeps the tracked changes and comments for review. ${ui.dim("For a clean copy, add --final.")}`
2343
+ `Keeps the tracked changes and comments for review. ${ui.dim("For a clean copy, add --view accepted.")}`
2201
2344
  ] : []
2202
2345
  ].join("\n")
2203
2346
  };
@@ -2278,11 +2421,13 @@ async function cmdConvert(args) {
2278
2421
  `revise convert ${shellArg(input)} ${shellArg(target)}`
2279
2422
  );
2280
2423
  }
2281
- const final = args.flags.final === true || format !== "docx";
2424
+ const asked = viewFlag(args, "tracked");
2425
+ const view = asked === "tracked" && format !== "docx" ? "accepted" : asked;
2426
+ const final = view !== "tracked";
2282
2427
  const bytes = await convert(
2283
2428
  source ? { bytes: source.bytes, filename: `document.${source.format}` } : input,
2284
2429
  format,
2285
- { mode: final ? "final" : "review" }
2430
+ { view }
2286
2431
  );
2287
2432
  if (out2 === null) await (io.writeStdout ?? defaultIo().writeStdout)(bytes);
2288
2433
  else writeOutput(args, out2, bytes);
@@ -2294,7 +2439,8 @@ async function cmdConvert(args) {
2294
2439
  outputWritten: out2 !== null,
2295
2440
  format: formatName(format),
2296
2441
  byteCount: bytes.length,
2297
- final
2442
+ final,
2443
+ view
2298
2444
  },
2299
2445
  // The document is the output; a report on stdout would corrupt it.
2300
2446
  text: () => out2 === null ? "" : `${ui.green("Wrote")} ${shellArg(target)} (${sizeText(bytes.length)})${source ? ` from stdin (${source.format})` : ""}.`
@@ -2422,7 +2568,8 @@ async function cmdGrep(args) {
2422
2568
  );
2423
2569
  }
2424
2570
  }
2425
- const hits = loaded.doc.grep(matcher, { ignoreCase });
2571
+ const view = viewFlag(args, "tracked");
2572
+ const hits = loaded.doc.grep(matcher, { ignoreCase, view });
2426
2573
  rememberReadBlocks(
2427
2574
  loaded.meta,
2428
2575
  hits.map((hit) => `<p id="${hit.id}">`).join("")
@@ -2434,28 +2581,38 @@ async function cmdGrep(args) {
2434
2581
  pattern,
2435
2582
  regex,
2436
2583
  ignoreCase,
2584
+ view,
2437
2585
  matchCount,
2438
2586
  blocks: hits.map((hit) => ({
2439
2587
  blockId: hit.id,
2440
2588
  blockIndex: hit.blockIndex,
2441
2589
  matchCount: hit.count,
2442
- text: hit.text
2590
+ text: hit.text,
2591
+ matches: hit.matches
2443
2592
  }))
2444
2593
  },
2445
2594
  text: () => hits.length ? [
2446
2595
  ...hits.map(
2447
- (hit) => `${ui.cyan(hit.id)} ${highlight(hit.text, matcher, ignoreCase)}`
2596
+ (hit) => `${ui.cyan(hit.id)} ${highlight(hit.text, matcher, ignoreCase)}${inChanges(hit.matches)}`
2448
2597
  ),
2449
2598
  "",
2450
2599
  `${plural(matchCount, "match", "matches")} in ${plural(hits.length, "block")}.`,
2451
2600
  nextLine(
2452
2601
  "Edit",
2453
- `revise replace-text ${loaded.shown} "<find>" "<replace>"`,
2602
+ `revise replace-all ${loaded.shown} "<find>" "<replace>"`,
2454
2603
  `revise call ${loaded.shown} replace '{"id":"<block-id>","replacements":[{"find":"\u2026","replace":"\u2026"}]}'`
2455
2604
  )
2456
2605
  ].join("\n") : `No matches for "${pattern}".`
2457
2606
  };
2458
2607
  }
2608
+ function inChanges(matches) {
2609
+ const count = (change) => matches.filter((m) => m.change === change).length;
2610
+ const parts = [
2611
+ count("deleted") ? `${count("deleted")} in deleted text` : "",
2612
+ count("inserted") ? `${count("inserted")} in inserted text` : ""
2613
+ ].filter(Boolean);
2614
+ return parts.length ? ui.dim(` (${parts.join(", ")})`) : "";
2615
+ }
2459
2616
  function highlight(text, matcher, ignoreCase) {
2460
2617
  if (!ui.enabled) return text;
2461
2618
  const re = typeof matcher === "string" ? new RegExp(
@@ -2464,15 +2621,19 @@ function highlight(text, matcher, ignoreCase) {
2464
2621
  ) : matcher;
2465
2622
  return text.replace(re, (m) => m ? ui.bold(ui.yellow(m)) : m);
2466
2623
  }
2467
- async function cmdReplaceText(args) {
2624
+ async function cmdReplaceAll(args) {
2468
2625
  const [find, replacement] = args.positional;
2469
- if (!find || replacement === void 0) throw usageError("replace-text");
2626
+ if (!find || replacement === void 0) throw usageError("replace-all");
2470
2627
  const loaded = await loadDocument(args);
2471
- const ids = flagString(args, "ids")?.split(",").map((id) => id.trim());
2628
+ const blocks = blockIdsFlag(args, "replace-all");
2472
2629
  const { result, checkpoint } = await mutate(
2473
2630
  loaded,
2474
- "replace-text",
2475
- () => loaded.doc.replaceText(find, replacement, { ids }),
2631
+ "replace-all",
2632
+ () => loaded.doc.replaceAll(find, replacement, {
2633
+ blocks,
2634
+ ignoreCase: args.flags["ignore-case"] === true,
2635
+ exact: args.flags.exact === true
2636
+ }),
2476
2637
  (r) => `Replaced "${find}" with "${replacement}" in ${plural(r.blocks.length - r.failures.length, "block")}`
2477
2638
  );
2478
2639
  const data = {
@@ -2481,19 +2642,33 @@ async function cmdReplaceText(args) {
2481
2642
  replace: replacement,
2482
2643
  matchedBlockIds: result.blocks,
2483
2644
  replacedOccurrences: result.replaced,
2645
+ regions: result.regions,
2646
+ nearMisses: result.nearMisses,
2484
2647
  changeIds: result.changeIds,
2485
2648
  failures: result.failures,
2486
2649
  saved: checkpoint !== null,
2487
2650
  checkpoint: currentCheckpoint(loaded.meta)
2488
2651
  };
2489
2652
  const edited = result.blocks.length - result.failures.length;
2653
+ const elsewhere = [
2654
+ ["header", "the header"],
2655
+ ["footer", "the footer"],
2656
+ ["footnote", "footnotes"],
2657
+ ["endnote", "endnotes"]
2658
+ ].filter(([region]) => result.regions[region]).map(([region, label]) => `${result.regions[region]} in ${label}`);
2659
+ const missedLines = result.nearMisses.map(
2660
+ (m) => `${ui.yellow("Not replaced:")} ${plural(m.count, "match", "matches")} of "${m.text}" (${m.reason === "case" ? "different capitalization; add --ignore-case" : "different characters; drop --exact"}).`
2661
+ );
2490
2662
  const text = () => {
2491
- if (!result.blocks.length) return `No block contains "${find}".`;
2663
+ if (!result.blocks.length) {
2664
+ return [`No block contains "${find}".`, ...missedLines].join("\n");
2665
+ }
2492
2666
  if (find === replacement || !result.replaced) {
2493
2667
  return result.failures.length ? "" : "Already matches: nothing to change. File not rewritten.";
2494
2668
  }
2495
2669
  return [
2496
- `${ui.green("Replaced")} ${plural(result.replaced, "occurrence")} of "${find}" in ${plural(edited, "block")}${loaded.meta.mode === "suggesting" ? " as tracked changes" : ""}.`,
2670
+ `${ui.green("Replaced")} ${plural(result.replaced, "occurrence")} of "${find}" in ${plural(edited, "block")}${elsewhere.length ? ` (${elsewhere.join(", ")})` : ""}${loaded.meta.mode === "suggesting" ? " as tracked changes" : ""}.`,
2671
+ ...missedLines,
2497
2672
  ...result.changeIds.length ? [
2498
2673
  result.changeIds.length <= 6 ? `Tracked changes: ${result.changeIds.map((id) => ui.cyan(id)).join(", ")}` : `${plural(result.changeIds.length, "tracked change")}. ${ui.dim("List:")} ${ui.cyan(`revise changes ${loaded.shown}`)}`
2499
2674
  ] : [],
@@ -2508,7 +2683,7 @@ async function cmdReplaceText(args) {
2508
2683
  error: {
2509
2684
  code: "replace_failed",
2510
2685
  message: `Could not edit ${plural(result.failures.length, "block")}: ${result.failures.map((f) => `${f.blockId} (${f.message})`).join("; ")}${checkpoint !== null ? ` ${plural(edited, "other block was", "other blocks were")} edited and saved.` : " Nothing was saved."}`,
2511
- hint: `revise read ${loaded.shown} --ids ${result.failures.map((f) => f.blockId).join(",")}`,
2686
+ hint: `revise read ${loaded.shown} --blocks ${result.failures.map((f) => f.blockId).join(",")}`,
2512
2687
  details: { failures: result.failures }
2513
2688
  }
2514
2689
  };
@@ -2619,7 +2794,7 @@ var EXAMPLES = [
2619
2794
  },
2620
2795
  {
2621
2796
  description: "Rename a party everywhere, as tracked changes",
2622
- command: 'revise replace-text contract.docx "Acme" "Northstar" --author "Legal bot"'
2797
+ command: 'revise replace-all contract.docx "Acme" "Northstar" --author "Legal bot"'
2623
2798
  },
2624
2799
  {
2625
2800
  description: "Run any editing tool (revise tools lists them): here, a page-numbered footer",
@@ -2684,8 +2859,11 @@ async function cmdSkill(args, install) {
2684
2859
  text: () => `${ui.green("Installed")} ${dir}/ (SKILL.md and reference/)`
2685
2860
  };
2686
2861
  }
2862
+ var RENAMED_COMMANDS = {
2863
+ "replace-text": "replace-all"
2864
+ };
2687
2865
  var OUT_COMMANDS = /* @__PURE__ */ new Set([
2688
- "replace-text",
2866
+ "replace-all",
2689
2867
  "call",
2690
2868
  "accept",
2691
2869
  "reject",
@@ -2811,6 +2989,18 @@ var FORCE = [
2811
2989
  "--force",
2812
2990
  "Replace the output file if it exists"
2813
2991
  ];
2992
+ var SECTION_OPTION = [
2993
+ "--section <s>",
2994
+ 'A heading and its subsections: "8", "8.1", or its title'
2995
+ ];
2996
+ var BLOCKS_OPTION = [
2997
+ "--blocks <b>",
2998
+ "Block ids and ranges: id,id or first..last"
2999
+ ];
3000
+ var VIEW_OPTION = [
3001
+ "--view <view>",
3002
+ "tracked (default): pending changes marked; accepted; or original"
3003
+ ];
2814
3004
  var OUT_OPTION = [
2815
3005
  "--out <file>",
2816
3006
  "Save the result as a new file (same format) and leave this one untouched; --force replaces an existing one"
@@ -2835,36 +3025,55 @@ var COMMAND_HELP = [
2835
3025
  {
2836
3026
  name: "read",
2837
3027
  group: "Reading",
2838
- usage: "revise read <file> [<index>] [--ids <id,id>]",
2839
- summary: "Blocks as HTML with block ids",
3028
+ usage: "revise read <file> [<index>] [--blocks <id,id>] [--view <view>]",
3029
+ summary: "Blocks as HTML with block ids; pending changes as <ins>/<del>",
2840
3030
  options: [
2841
3031
  ["<index>", "Block index to start from (0-based, default 0)"],
2842
- ["--ids <id,id>", "Read these blocks instead"]
3032
+ ["--blocks <id,id>", "Read these blocks instead"],
3033
+ VIEW_OPTION
2843
3034
  ]
2844
3035
  },
2845
3036
  {
2846
3037
  name: "grep",
2847
3038
  group: "Reading",
2848
- usage: 'revise grep <file> "<text>" [--regex] [--ignore-case]',
3039
+ usage: 'revise grep <file> "<text>" [--regex] [--ignore-case] [--view <view>]',
2849
3040
  summary: "Matching blocks as plain text with ids",
2850
3041
  options: [
2851
3042
  ["--regex", "Treat the text as a JavaScript regular expression"],
2852
- ["--ignore-case", "Match regardless of case"]
3043
+ ["--ignore-case", "Match regardless of case"],
3044
+ [
3045
+ "--view <view>",
3046
+ "tracked (default): matches deleted and inserted text, [-marked-]{+inline+}; accepted; or original"
3047
+ ]
2853
3048
  ]
2854
3049
  },
2855
3050
  {
2856
3051
  name: "search",
2857
3052
  group: "Reading",
2858
- usage: 'revise search <file> "<text>" ["<text>" ...] [--page <n>]',
3053
+ usage: 'revise search <file> "<text>" ["<text>" ...] [--results-page <n>] [--view <view>]',
2859
3054
  summary: "Matching blocks as HTML, with a search result id",
2860
- options: [["--page <n>", "Page of results (1-based)"]]
3055
+ options: [
3056
+ ["--results-page <n>", "Page of results (1-based)"],
3057
+ [
3058
+ "--view <view>",
3059
+ "tracked (default): also lists matches in deleted text; accepted; or original (no result id)"
3060
+ ]
3061
+ ]
2861
3062
  },
2862
3063
  {
2863
- name: "replace-text",
3064
+ name: "replace-all",
2864
3065
  group: "Editing (saves the file)",
2865
- usage: 'revise replace-text <file> "<find>" "<replace>" [--ids <id,id>]',
2866
- summary: "Replace every exact occurrence",
2867
- options: [["--ids <id,id>", "Only in these blocks"], OUT_OPTION]
3066
+ usage: 'revise replace-all <file> "<find>" "<replace>" [--ignore-case] [--exact] [--blocks <id,id>]',
3067
+ summary: "Replace every occurrence: body, headers, footers, notes",
3068
+ options: [
3069
+ ["--ignore-case", "Also PROVIDER and provider"],
3070
+ [
3071
+ "--exact",
3072
+ 'Quotes and spaces must match too (by default \u201C and ", no-break and plain spaces match)'
3073
+ ],
3074
+ ["--blocks <id,id>", "Only in these blocks"],
3075
+ OUT_OPTION
3076
+ ]
2868
3077
  },
2869
3078
  {
2870
3079
  name: "call",
@@ -2892,10 +3101,11 @@ var COMMAND_HELP = [
2892
3101
  {
2893
3102
  name: "changes",
2894
3103
  group: "Review",
2895
- usage: "revise changes <file> [--section <s>] [--details]",
3104
+ usage: "revise changes <file> [--section <s>] [--blocks <b>] [--details]",
2896
3105
  summary: "Tracked changes: where, and what text",
2897
3106
  options: [
2898
- ["--section <s>", 'Only in one section: "8", "8.", or its title'],
3107
+ SECTION_OPTION,
3108
+ BLOCKS_OPTION,
2899
3109
  ["--details", "List every change, even when there are many"]
2900
3110
  ]
2901
3111
  },
@@ -2914,12 +3124,13 @@ var COMMAND_HELP = [
2914
3124
  {
2915
3125
  name: "accept",
2916
3126
  group: "Review",
2917
- usage: "revise accept <file> (<change-id> ... | --all | --section <s> | --except-section <s>) [--dry-run]",
3127
+ usage: "revise accept <file> (<change-id> ... | --all | --section <s> | --except-section <s> | --blocks <b>) [--dry-run]",
2918
3128
  summary: "Accept tracked changes",
2919
3129
  options: [
2920
3130
  ["--all", "Every tracked change"],
2921
- ["--section <s>", 'Changes in one section: "8", "8.", or its title'],
3131
+ SECTION_OPTION,
2922
3132
  ["--except-section <s>", "Every change outside one section"],
3133
+ BLOCKS_OPTION,
2923
3134
  ["--dry-run", "Show what would be accepted; change nothing"],
2924
3135
  OUT_OPTION
2925
3136
  ]
@@ -2927,12 +3138,13 @@ var COMMAND_HELP = [
2927
3138
  {
2928
3139
  name: "reject",
2929
3140
  group: "Review",
2930
- usage: "revise reject <file> (<change-id> ... | --all | --section <s> | --except-section <s>) [--dry-run]",
3141
+ usage: "revise reject <file> (<change-id> ... | --all | --section <s> | --except-section <s> | --blocks <b>) [--dry-run]",
2931
3142
  summary: "Reject tracked changes",
2932
3143
  options: [
2933
3144
  ["--all", "Every tracked change"],
2934
- ["--section <s>", 'Changes in one section: "8", "8.", or its title'],
3145
+ SECTION_OPTION,
2935
3146
  ["--except-section <s>", "Every change outside one section"],
3147
+ BLOCKS_OPTION,
2936
3148
  ["--dry-run", "Show what would be rejected; change nothing"],
2937
3149
  OUT_OPTION
2938
3150
  ]
@@ -2963,7 +3175,7 @@ var COMMAND_HELP = [
2963
3175
  options: [
2964
3176
  ["--page <n>", "Page to render (1-based, default 1)"],
2965
3177
  ["--pages <n-m>", "Up to 6 pages in one image"],
2966
- ["--view <view>", "tracked (default), accepted, or original"],
3178
+ VIEW_OPTION,
2967
3179
  ["--layout", "Outline layout boxes"],
2968
3180
  ["--detailed", "Full resolution for several pages"],
2969
3181
  ["--out <file>", "Where to save it (default: inside .revise/)"]
@@ -2972,20 +3184,26 @@ var COMMAND_HELP = [
2972
3184
  {
2973
3185
  name: "export",
2974
3186
  group: "Output",
2975
- usage: "revise export <file> <output> [--final] [--force]",
3187
+ usage: "revise export <file> <output> [--view <view>] [--force]",
2976
3188
  summary: "Write a copy as .docx, .pdf, .md, .html, or .txt",
2977
- options: [["--final", "Accept every tracked change in the copy"], FORCE]
3189
+ options: [
3190
+ [
3191
+ "--view <view>",
3192
+ "tracked (default; only .docx keeps changes), accepted, or original"
3193
+ ],
3194
+ FORCE
3195
+ ]
2978
3196
  },
2979
3197
  {
2980
3198
  name: "new",
2981
3199
  group: "Create and convert",
2982
- usage: "revise new <output> [--from <draft.html|.md>] [options]",
3200
+ usage: "revise new <output> [--content <draft.html|.md>] [options]",
2983
3201
  summary: "Create a document, in edit mode",
2984
3202
  options: [
2985
- ["--from <file>", "Start from HTML or Markdown"],
3203
+ ["--content <file>", "Start from HTML or Markdown"],
2986
3204
  ["--title <text>", "Document title"],
2987
3205
  ["--mode suggest", "Start in suggest mode instead"],
2988
- ["--page <size>", "letter, a4, legal, ..."],
3206
+ ["--page-size <size>", "letter, a4, legal, ..."],
2989
3207
  ["--orientation <o>", "portrait or landscape"],
2990
3208
  ["--margins <length>", "For example 1in or 2.5cm"],
2991
3209
  ["--header <text>", "Header text (also --header-left/-center/-right)"],
@@ -3007,10 +3225,13 @@ var COMMAND_HELP = [
3007
3225
  {
3008
3226
  name: "convert",
3009
3227
  group: "Create and convert",
3010
- usage: "revise convert <input|-> <output|-> [--final] [--force]",
3011
- summary: "Convert between .docx, .md, .html, .odt, .rtf, .txt, .pdf",
3228
+ usage: "revise convert <input|-> <output|-> [--view <view>] [--force]",
3229
+ summary: "Convert .docx .md .html .odt .rtf .txt to .docx .md .html .txt .pdf",
3012
3230
  options: [
3013
- ["--final", "Accept every tracked change"],
3231
+ [
3232
+ "--view <view>",
3233
+ "tracked (default; only .docx keeps changes), accepted, or original"
3234
+ ],
3014
3235
  FORCE,
3015
3236
  [
3016
3237
  "--from <format>",
@@ -3146,7 +3367,7 @@ var FILE_COMMANDS = /* @__PURE__ */ new Set([
3146
3367
  "read",
3147
3368
  "grep",
3148
3369
  "search",
3149
- "replace-text",
3370
+ "replace-all",
3150
3371
  "call",
3151
3372
  "mode",
3152
3373
  "changes",
@@ -3313,7 +3534,11 @@ async function main(argv, cliIo) {
3313
3534
  };
3314
3535
  try {
3315
3536
  const args = parseArgs(argv);
3316
- const first = args.positional.shift();
3537
+ const typed = args.positional.shift();
3538
+ const first = typed && RENAMED_COMMANDS[typed] ? RENAMED_COMMANDS[typed] : typed;
3539
+ if (first === "help" && args.positional[0]) {
3540
+ args.positional[0] = RENAMED_COMMANDS[args.positional[0]] ?? args.positional[0];
3541
+ }
3317
3542
  if ((args.flags.version || first === "version") && !args.flags.help) {
3318
3543
  command = "version";
3319
3544
  const { version, build } = producer();
@@ -3374,7 +3599,7 @@ async function main(argv, cliIo) {
3374
3599
  resolved = "skill install";
3375
3600
  }
3376
3601
  if (args.flags.version) resolved = "version";
3377
- command = resolved;
3602
+ command = typed && RENAMED_COMMANDS[typed] ? typed : resolved;
3378
3603
  currentCommand = resolved;
3379
3604
  if (shorthandTool) {
3380
3605
  const definition = await toolDefinition(shorthandTool);
@@ -3458,8 +3683,8 @@ ${optionLines(GLOBAL_OPTIONS).join("\n")}`
3458
3683
  return cmdCompare(args);
3459
3684
  case "grep":
3460
3685
  return cmdGrep(args);
3461
- case "replace-text":
3462
- return cmdReplaceText(args);
3686
+ case "replace-all":
3687
+ return cmdReplaceAll(args);
3463
3688
  case "xml ls":
3464
3689
  return cmdXml(args, "ls");
3465
3690
  case "xml cat":