revise 3.0.1 → 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';
@@ -662,6 +663,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
662
663
  "dry-run",
663
664
  "regex",
664
665
  "ignore-case",
666
+ "exact",
665
667
  "json",
666
668
  "all",
667
669
  "final",
@@ -675,6 +677,10 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
675
677
  ]);
676
678
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
677
679
  "author",
680
+ "blocks",
681
+ "content",
682
+ "page-size",
683
+ "results-page",
678
684
  "dir",
679
685
  "except-section",
680
686
  "file",
@@ -739,6 +745,33 @@ function flagString(args, name) {
739
745
  const value = args.flags[name];
740
746
  return typeof value === "string" ? value : void 0;
741
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
+ }
742
775
  function flagInt(args, name) {
743
776
  const value = flagString(args, name);
744
777
  if (value === void 0) return void 0;
@@ -837,8 +870,8 @@ var ui = PLAIN;
837
870
  var uiErr = PLAIN;
838
871
  function producer() {
839
872
  return {
840
- version: true ? "3.0.1" : "dev",
841
- build: true ? "7ef88f310" : null
873
+ version: true ? "3.1.0" : "dev",
874
+ build: true ? "7cded52c5" : null
842
875
  };
843
876
  }
844
877
  var warnings = [];
@@ -1249,7 +1282,7 @@ async function restoreReadContext(loaded, input) {
1249
1282
  }
1250
1283
  }
1251
1284
  function cliVocabulary(message, file) {
1252
- 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}`);
1253
1286
  }
1254
1287
  function toolData(loaded, tool, fields) {
1255
1288
  return { file: loaded.meta.file, tool, ...fields };
@@ -1309,7 +1342,7 @@ async function runTool(loaded, name, input, options = {}) {
1309
1342
  const { result, checkpoint } = await mutate(
1310
1343
  loaded,
1311
1344
  name,
1312
- () => loaded.doc.tools.run(name, input, options),
1345
+ () => loaded.doc.tools.run(name, input, { direct: options.direct }),
1313
1346
  (r) => "error" in r ? r.error.message : r.value.message ?? name,
1314
1347
  { saveIf: (r) => !("error" in r) }
1315
1348
  );
@@ -1360,16 +1393,19 @@ async function runTool(loaded, name, input, options = {}) {
1360
1393
  value = persistSearch(loaded.meta, input, value);
1361
1394
  if (value.context?.html) rememberReadBlocks(loaded.meta, value.context.html);
1362
1395
  const changeIds = value.suggestionIds ?? [];
1363
- 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;
1364
1397
  return {
1365
- data: toolData(loaded, name, {
1366
- saved: checkpoint !== null,
1367
- checkpoint: now,
1368
- message: value.message,
1369
- changeIds,
1370
- html,
1371
- result: value.data ?? null
1372
- }),
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
+ },
1373
1409
  text: () => {
1374
1410
  const lines = [];
1375
1411
  if (value.message)
@@ -1455,7 +1491,7 @@ async function cmdNew(args) {
1455
1491
  if (requestedMode && !["suggest", "edit"].includes(requestedMode)) {
1456
1492
  throw fail("--mode must be suggest or edit.", "usage", "revise new --help");
1457
1493
  }
1458
- const from = flagString(args, "from");
1494
+ const from = flagString(args, "content") ?? flagString(args, "from");
1459
1495
  if (from !== void 0 && !fs2.existsSync(from)) {
1460
1496
  throw fail(`No such file: ${from}`, "file_not_found");
1461
1497
  }
@@ -1510,11 +1546,11 @@ function newDocumentSteps(args, probe) {
1510
1546
  const layout = probe.tools.definitions().find((tool) => tool.name === "set_page_layout");
1511
1547
  const allowed = (property) => layout?.inputSchema.properties?.[property]?.enum ?? [];
1512
1548
  const layoutInput = {};
1513
- const page = flagString(args, "page")?.toLowerCase();
1549
+ const page = (flagString(args, "page-size") ?? flagString(args, "page"))?.toLowerCase();
1514
1550
  if (page !== void 0) {
1515
1551
  if (!allowed("pageSize").includes(page)) {
1516
1552
  throw fail(
1517
- `--page must be one of: ${allowed("pageSize").join(", ")}.`,
1553
+ `--page-size must be one of: ${allowed("pageSize").join(", ")}.`,
1518
1554
  "usage",
1519
1555
  "revise new --help"
1520
1556
  );
@@ -1657,22 +1693,111 @@ async function cmdStatus(args) {
1657
1693
  }
1658
1694
  async function cmdRead(args) {
1659
1695
  const loaded = await loadDocument(args);
1660
- const ids = flagString(args, "ids");
1696
+ const ids = blockIdsFlag(args, "read");
1697
+ const view = viewFlag(args, "tracked");
1661
1698
  if (ids) {
1662
- return runTool(loaded, "read_specific_blocks", {
1663
- block_ids: ids.split(",").map((id) => id.trim()).filter(Boolean)
1664
- });
1699
+ return runTool(
1700
+ loaded,
1701
+ "read_specific_blocks",
1702
+ { block_ids: ids },
1703
+ { view }
1704
+ );
1665
1705
  }
1666
1706
  const index = strictInt(args.positional[0] ?? "0", "The block index");
1667
- return runTool(loaded, "read_blocks_from_index", { index });
1707
+ return runTool(loaded, "read_blocks_from_index", { index }, { view });
1668
1708
  }
1669
1709
  async function cmdSearch(args) {
1670
1710
  if (!args.positional.length) throw usageError("search");
1711
+ const view = viewFlag(args, "tracked");
1671
1712
  const loaded = await loadDocument(args);
1672
- return runTool(loaded, "search_document", {
1673
- queries: args.positional,
1674
- 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
+ }));
1675
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
+ }
1676
1801
  }
1677
1802
  async function cmdOutline(args) {
1678
1803
  const loaded = await loadDocument(args);
@@ -1807,8 +1932,11 @@ var BEFORE_FIRST_HEADING = "(before the first heading)";
1807
1932
  async function cmdChanges(args) {
1808
1933
  const loaded = await loadDocument(args);
1809
1934
  const section = flagString(args, "section");
1810
- const changes = await loaded.doc.changes({ section });
1811
- 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;
1812
1940
  const entries = changes.map((c) => ({
1813
1941
  changeId: c.id,
1814
1942
  type: c.type,
@@ -1824,17 +1952,18 @@ async function cmdChanges(args) {
1824
1952
  `revise accept ${loaded.shown} <change-id...>`,
1825
1953
  `revise reject ${loaded.shown} <change-id...>`
1826
1954
  ) + `
1827
- ${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)`)}`;
1828
1956
  return {
1829
1957
  data: {
1830
1958
  file: loaded.meta.file,
1831
1959
  section: section ?? null,
1960
+ blocks: blocksFlag ?? null,
1832
1961
  trackedChangeCount: entries.length,
1833
1962
  changes: entries
1834
1963
  },
1835
1964
  text: () => {
1836
1965
  if (!entries.length) {
1837
- 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.";
1838
1967
  }
1839
1968
  if (!details && entries.length > 30) {
1840
1969
  return [
@@ -1885,10 +2014,18 @@ async function cmdSettle(args, accept) {
1885
2014
  const all = args.flags.all === true;
1886
2015
  const section = flagString(args, "section");
1887
2016
  const exceptSection = flagString(args, "except-section");
1888
- 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) {
1889
2020
  throw usageError(action);
1890
2021
  }
1891
- const selection = { ids: args.positional, all, section, exceptSection };
2022
+ const selection = {
2023
+ ids: args.positional,
2024
+ all,
2025
+ section,
2026
+ exceptSection,
2027
+ blocks
2028
+ };
1892
2029
  const base = {
1893
2030
  file: loaded.meta.file,
1894
2031
  action
@@ -1955,7 +2092,7 @@ async function cmdSettle(args, accept) {
1955
2092
  const done = accept ? "Accepted" : "Rejected";
1956
2093
  const failedOutright = !result.resolved.length && (result.missing.length || failed.length);
1957
2094
  const text = () => failedOutright ? "" : [
1958
- 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 + ".",
1959
2096
  ...checkpoint !== null ? [savedLine(loaded, checkpoint)] : []
1960
2097
  ].join("\n");
1961
2098
  if (result.missing.length || failed.length) {
@@ -2154,7 +2291,7 @@ async function cmdExport(args) {
2154
2291
  assertCanOverwrite(args, out2);
2155
2292
  const result = await loaded.doc.export({
2156
2293
  format,
2157
- mode: args.flags.final === true ? "final" : "review"
2294
+ view: viewFlag(args, "tracked")
2158
2295
  });
2159
2296
  writeOutput(args, out2, result.bytes);
2160
2297
  let summary = null;
@@ -2194,15 +2331,16 @@ async function cmdExport(args) {
2194
2331
  format: formatName(format),
2195
2332
  byteCount: result.bytes.length,
2196
2333
  final: result.mode === "final",
2334
+ view: result.view,
2197
2335
  trackedChangeCount,
2198
2336
  docxExport,
2199
2337
  summary
2200
2338
  },
2201
2339
  text: () => [
2202
- `${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" : ""}).`,
2203
2341
  ...summary ? [summaryText(summary)] : [],
2204
2342
  ...format === "docx" && trackedChangeCount ? [
2205
- `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.")}`
2206
2344
  ] : []
2207
2345
  ].join("\n")
2208
2346
  };
@@ -2283,11 +2421,13 @@ async function cmdConvert(args) {
2283
2421
  `revise convert ${shellArg(input)} ${shellArg(target)}`
2284
2422
  );
2285
2423
  }
2286
- 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";
2287
2427
  const bytes = await convert(
2288
2428
  source ? { bytes: source.bytes, filename: `document.${source.format}` } : input,
2289
2429
  format,
2290
- { mode: final ? "final" : "review" }
2430
+ { view }
2291
2431
  );
2292
2432
  if (out2 === null) await (io.writeStdout ?? defaultIo().writeStdout)(bytes);
2293
2433
  else writeOutput(args, out2, bytes);
@@ -2299,7 +2439,8 @@ async function cmdConvert(args) {
2299
2439
  outputWritten: out2 !== null,
2300
2440
  format: formatName(format),
2301
2441
  byteCount: bytes.length,
2302
- final
2442
+ final,
2443
+ view
2303
2444
  },
2304
2445
  // The document is the output; a report on stdout would corrupt it.
2305
2446
  text: () => out2 === null ? "" : `${ui.green("Wrote")} ${shellArg(target)} (${sizeText(bytes.length)})${source ? ` from stdin (${source.format})` : ""}.`
@@ -2427,7 +2568,8 @@ async function cmdGrep(args) {
2427
2568
  );
2428
2569
  }
2429
2570
  }
2430
- const hits = loaded.doc.grep(matcher, { ignoreCase });
2571
+ const view = viewFlag(args, "tracked");
2572
+ const hits = loaded.doc.grep(matcher, { ignoreCase, view });
2431
2573
  rememberReadBlocks(
2432
2574
  loaded.meta,
2433
2575
  hits.map((hit) => `<p id="${hit.id}">`).join("")
@@ -2439,28 +2581,38 @@ async function cmdGrep(args) {
2439
2581
  pattern,
2440
2582
  regex,
2441
2583
  ignoreCase,
2584
+ view,
2442
2585
  matchCount,
2443
2586
  blocks: hits.map((hit) => ({
2444
2587
  blockId: hit.id,
2445
2588
  blockIndex: hit.blockIndex,
2446
2589
  matchCount: hit.count,
2447
- text: hit.text
2590
+ text: hit.text,
2591
+ matches: hit.matches
2448
2592
  }))
2449
2593
  },
2450
2594
  text: () => hits.length ? [
2451
2595
  ...hits.map(
2452
- (hit) => `${ui.cyan(hit.id)} ${highlight(hit.text, matcher, ignoreCase)}`
2596
+ (hit) => `${ui.cyan(hit.id)} ${highlight(hit.text, matcher, ignoreCase)}${inChanges(hit.matches)}`
2453
2597
  ),
2454
2598
  "",
2455
2599
  `${plural(matchCount, "match", "matches")} in ${plural(hits.length, "block")}.`,
2456
2600
  nextLine(
2457
2601
  "Edit",
2458
- `revise replace-text ${loaded.shown} "<find>" "<replace>"`,
2602
+ `revise replace-all ${loaded.shown} "<find>" "<replace>"`,
2459
2603
  `revise call ${loaded.shown} replace '{"id":"<block-id>","replacements":[{"find":"\u2026","replace":"\u2026"}]}'`
2460
2604
  )
2461
2605
  ].join("\n") : `No matches for "${pattern}".`
2462
2606
  };
2463
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
+ }
2464
2616
  function highlight(text, matcher, ignoreCase) {
2465
2617
  if (!ui.enabled) return text;
2466
2618
  const re = typeof matcher === "string" ? new RegExp(
@@ -2469,15 +2621,19 @@ function highlight(text, matcher, ignoreCase) {
2469
2621
  ) : matcher;
2470
2622
  return text.replace(re, (m) => m ? ui.bold(ui.yellow(m)) : m);
2471
2623
  }
2472
- async function cmdReplaceText(args) {
2624
+ async function cmdReplaceAll(args) {
2473
2625
  const [find, replacement] = args.positional;
2474
- if (!find || replacement === void 0) throw usageError("replace-text");
2626
+ if (!find || replacement === void 0) throw usageError("replace-all");
2475
2627
  const loaded = await loadDocument(args);
2476
- const ids = flagString(args, "ids")?.split(",").map((id) => id.trim());
2628
+ const blocks = blockIdsFlag(args, "replace-all");
2477
2629
  const { result, checkpoint } = await mutate(
2478
2630
  loaded,
2479
- "replace-text",
2480
- () => 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
+ }),
2481
2637
  (r) => `Replaced "${find}" with "${replacement}" in ${plural(r.blocks.length - r.failures.length, "block")}`
2482
2638
  );
2483
2639
  const data = {
@@ -2486,19 +2642,33 @@ async function cmdReplaceText(args) {
2486
2642
  replace: replacement,
2487
2643
  matchedBlockIds: result.blocks,
2488
2644
  replacedOccurrences: result.replaced,
2645
+ regions: result.regions,
2646
+ nearMisses: result.nearMisses,
2489
2647
  changeIds: result.changeIds,
2490
2648
  failures: result.failures,
2491
2649
  saved: checkpoint !== null,
2492
2650
  checkpoint: currentCheckpoint(loaded.meta)
2493
2651
  };
2494
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
+ );
2495
2662
  const text = () => {
2496
- if (!result.blocks.length) return `No block contains "${find}".`;
2663
+ if (!result.blocks.length) {
2664
+ return [`No block contains "${find}".`, ...missedLines].join("\n");
2665
+ }
2497
2666
  if (find === replacement || !result.replaced) {
2498
2667
  return result.failures.length ? "" : "Already matches: nothing to change. File not rewritten.";
2499
2668
  }
2500
2669
  return [
2501
- `${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,
2502
2672
  ...result.changeIds.length ? [
2503
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}`)}`
2504
2674
  ] : [],
@@ -2513,7 +2683,7 @@ async function cmdReplaceText(args) {
2513
2683
  error: {
2514
2684
  code: "replace_failed",
2515
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."}`,
2516
- 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(",")}`,
2517
2687
  details: { failures: result.failures }
2518
2688
  }
2519
2689
  };
@@ -2624,7 +2794,7 @@ var EXAMPLES = [
2624
2794
  },
2625
2795
  {
2626
2796
  description: "Rename a party everywhere, as tracked changes",
2627
- command: 'revise replace-text contract.docx "Acme" "Northstar" --author "Legal bot"'
2797
+ command: 'revise replace-all contract.docx "Acme" "Northstar" --author "Legal bot"'
2628
2798
  },
2629
2799
  {
2630
2800
  description: "Run any editing tool (revise tools lists them): here, a page-numbered footer",
@@ -2689,8 +2859,11 @@ async function cmdSkill(args, install) {
2689
2859
  text: () => `${ui.green("Installed")} ${dir}/ (SKILL.md and reference/)`
2690
2860
  };
2691
2861
  }
2862
+ var RENAMED_COMMANDS = {
2863
+ "replace-text": "replace-all"
2864
+ };
2692
2865
  var OUT_COMMANDS = /* @__PURE__ */ new Set([
2693
- "replace-text",
2866
+ "replace-all",
2694
2867
  "call",
2695
2868
  "accept",
2696
2869
  "reject",
@@ -2816,6 +2989,18 @@ var FORCE = [
2816
2989
  "--force",
2817
2990
  "Replace the output file if it exists"
2818
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
+ ];
2819
3004
  var OUT_OPTION = [
2820
3005
  "--out <file>",
2821
3006
  "Save the result as a new file (same format) and leave this one untouched; --force replaces an existing one"
@@ -2840,36 +3025,55 @@ var COMMAND_HELP = [
2840
3025
  {
2841
3026
  name: "read",
2842
3027
  group: "Reading",
2843
- usage: "revise read <file> [<index>] [--ids <id,id>]",
2844
- 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>",
2845
3030
  options: [
2846
3031
  ["<index>", "Block index to start from (0-based, default 0)"],
2847
- ["--ids <id,id>", "Read these blocks instead"]
3032
+ ["--blocks <id,id>", "Read these blocks instead"],
3033
+ VIEW_OPTION
2848
3034
  ]
2849
3035
  },
2850
3036
  {
2851
3037
  name: "grep",
2852
3038
  group: "Reading",
2853
- usage: 'revise grep <file> "<text>" [--regex] [--ignore-case]',
3039
+ usage: 'revise grep <file> "<text>" [--regex] [--ignore-case] [--view <view>]',
2854
3040
  summary: "Matching blocks as plain text with ids",
2855
3041
  options: [
2856
3042
  ["--regex", "Treat the text as a JavaScript regular expression"],
2857
- ["--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
+ ]
2858
3048
  ]
2859
3049
  },
2860
3050
  {
2861
3051
  name: "search",
2862
3052
  group: "Reading",
2863
- usage: 'revise search <file> "<text>" ["<text>" ...] [--page <n>]',
3053
+ usage: 'revise search <file> "<text>" ["<text>" ...] [--results-page <n>] [--view <view>]',
2864
3054
  summary: "Matching blocks as HTML, with a search result id",
2865
- 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
+ ]
2866
3062
  },
2867
3063
  {
2868
- name: "replace-text",
3064
+ name: "replace-all",
2869
3065
  group: "Editing (saves the file)",
2870
- usage: 'revise replace-text <file> "<find>" "<replace>" [--ids <id,id>]',
2871
- summary: "Replace every exact occurrence",
2872
- 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
+ ]
2873
3077
  },
2874
3078
  {
2875
3079
  name: "call",
@@ -2897,10 +3101,11 @@ var COMMAND_HELP = [
2897
3101
  {
2898
3102
  name: "changes",
2899
3103
  group: "Review",
2900
- usage: "revise changes <file> [--section <s>] [--details]",
3104
+ usage: "revise changes <file> [--section <s>] [--blocks <b>] [--details]",
2901
3105
  summary: "Tracked changes: where, and what text",
2902
3106
  options: [
2903
- ["--section <s>", 'Only in one section: "8", "8.", or its title'],
3107
+ SECTION_OPTION,
3108
+ BLOCKS_OPTION,
2904
3109
  ["--details", "List every change, even when there are many"]
2905
3110
  ]
2906
3111
  },
@@ -2919,12 +3124,13 @@ var COMMAND_HELP = [
2919
3124
  {
2920
3125
  name: "accept",
2921
3126
  group: "Review",
2922
- 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]",
2923
3128
  summary: "Accept tracked changes",
2924
3129
  options: [
2925
3130
  ["--all", "Every tracked change"],
2926
- ["--section <s>", 'Changes in one section: "8", "8.", or its title'],
3131
+ SECTION_OPTION,
2927
3132
  ["--except-section <s>", "Every change outside one section"],
3133
+ BLOCKS_OPTION,
2928
3134
  ["--dry-run", "Show what would be accepted; change nothing"],
2929
3135
  OUT_OPTION
2930
3136
  ]
@@ -2932,12 +3138,13 @@ var COMMAND_HELP = [
2932
3138
  {
2933
3139
  name: "reject",
2934
3140
  group: "Review",
2935
- 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]",
2936
3142
  summary: "Reject tracked changes",
2937
3143
  options: [
2938
3144
  ["--all", "Every tracked change"],
2939
- ["--section <s>", 'Changes in one section: "8", "8.", or its title'],
3145
+ SECTION_OPTION,
2940
3146
  ["--except-section <s>", "Every change outside one section"],
3147
+ BLOCKS_OPTION,
2941
3148
  ["--dry-run", "Show what would be rejected; change nothing"],
2942
3149
  OUT_OPTION
2943
3150
  ]
@@ -2968,7 +3175,7 @@ var COMMAND_HELP = [
2968
3175
  options: [
2969
3176
  ["--page <n>", "Page to render (1-based, default 1)"],
2970
3177
  ["--pages <n-m>", "Up to 6 pages in one image"],
2971
- ["--view <view>", "tracked (default), accepted, or original"],
3178
+ VIEW_OPTION,
2972
3179
  ["--layout", "Outline layout boxes"],
2973
3180
  ["--detailed", "Full resolution for several pages"],
2974
3181
  ["--out <file>", "Where to save it (default: inside .revise/)"]
@@ -2977,20 +3184,26 @@ var COMMAND_HELP = [
2977
3184
  {
2978
3185
  name: "export",
2979
3186
  group: "Output",
2980
- usage: "revise export <file> <output> [--final] [--force]",
3187
+ usage: "revise export <file> <output> [--view <view>] [--force]",
2981
3188
  summary: "Write a copy as .docx, .pdf, .md, .html, or .txt",
2982
- 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
+ ]
2983
3196
  },
2984
3197
  {
2985
3198
  name: "new",
2986
3199
  group: "Create and convert",
2987
- usage: "revise new <output> [--from <draft.html|.md>] [options]",
3200
+ usage: "revise new <output> [--content <draft.html|.md>] [options]",
2988
3201
  summary: "Create a document, in edit mode",
2989
3202
  options: [
2990
- ["--from <file>", "Start from HTML or Markdown"],
3203
+ ["--content <file>", "Start from HTML or Markdown"],
2991
3204
  ["--title <text>", "Document title"],
2992
3205
  ["--mode suggest", "Start in suggest mode instead"],
2993
- ["--page <size>", "letter, a4, legal, ..."],
3206
+ ["--page-size <size>", "letter, a4, legal, ..."],
2994
3207
  ["--orientation <o>", "portrait or landscape"],
2995
3208
  ["--margins <length>", "For example 1in or 2.5cm"],
2996
3209
  ["--header <text>", "Header text (also --header-left/-center/-right)"],
@@ -3012,10 +3225,13 @@ var COMMAND_HELP = [
3012
3225
  {
3013
3226
  name: "convert",
3014
3227
  group: "Create and convert",
3015
- usage: "revise convert <input|-> <output|-> [--final] [--force]",
3016
- 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",
3017
3230
  options: [
3018
- ["--final", "Accept every tracked change"],
3231
+ [
3232
+ "--view <view>",
3233
+ "tracked (default; only .docx keeps changes), accepted, or original"
3234
+ ],
3019
3235
  FORCE,
3020
3236
  [
3021
3237
  "--from <format>",
@@ -3151,7 +3367,7 @@ var FILE_COMMANDS = /* @__PURE__ */ new Set([
3151
3367
  "read",
3152
3368
  "grep",
3153
3369
  "search",
3154
- "replace-text",
3370
+ "replace-all",
3155
3371
  "call",
3156
3372
  "mode",
3157
3373
  "changes",
@@ -3318,7 +3534,11 @@ async function main(argv, cliIo) {
3318
3534
  };
3319
3535
  try {
3320
3536
  const args = parseArgs(argv);
3321
- 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
+ }
3322
3542
  if ((args.flags.version || first === "version") && !args.flags.help) {
3323
3543
  command = "version";
3324
3544
  const { version, build } = producer();
@@ -3379,7 +3599,7 @@ async function main(argv, cliIo) {
3379
3599
  resolved = "skill install";
3380
3600
  }
3381
3601
  if (args.flags.version) resolved = "version";
3382
- command = resolved;
3602
+ command = typed && RENAMED_COMMANDS[typed] ? typed : resolved;
3383
3603
  currentCommand = resolved;
3384
3604
  if (shorthandTool) {
3385
3605
  const definition = await toolDefinition(shorthandTool);
@@ -3463,8 +3683,8 @@ ${optionLines(GLOBAL_OPTIONS).join("\n")}`
3463
3683
  return cmdCompare(args);
3464
3684
  case "grep":
3465
3685
  return cmdGrep(args);
3466
- case "replace-text":
3467
- return cmdReplaceText(args);
3686
+ case "replace-all":
3687
+ return cmdReplaceAll(args);
3468
3688
  case "xml ls":
3469
3689
  return cmdXml(args, "ls");
3470
3690
  case "xml cat":