npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stll/folio-cli

v0.4.1

Published

Command line and stdio MCP server for reading, reviewing, and redlining .docx files with folio: tracked-change edits, comments, and comparisons written atomically with version preconditions.

Readme

@stll/folio-cli

folio: a command line for reading and reviewing .docx files on disk. It runs the @stll/folio-agents tools over local files, so a script, a person at a terminal, and an agent see the same blocks, ids, and results.

Part of stella, an open-source legal workspace.

Install

bun add -g @stll/folio-cli   # or: npm install -g @stll/folio-cli
folio --help

Node.js 22 or later.

Reading

| Command | Tool | Prints | | ---------- | ---------------------- | ------------------------------------------------------ | | read | read_document | Body blocks with ids, text, and blockTextHash, paged | | outline | get_document_outline | Heading outline with section handles | | section | read_section | One section's blocks, by handle | | stories | list_stories | Header, footer, footnote, and endnote story handles | | story | read_story | One story's text, by handle | | find | find_text | Exact matches with range handles and context | | comments | read_comments | Comment threads with replies and resolved state | | changes | read_changes | Pending tracked changes |

folio read contract.docx --max-blocks 50
folio find contract.docx --query "Termination" --whole-word
# the handle is copied from `folio outline`
folio section contract.docx --handle '{"type":"headingSection","story":"main","headingBlockId":"1A2B3C4D","headingTextHash":"h5f3a9c","headingLevel":1}'
folio comments contract.docx --filter open --output text

Each command's flags are generated from its tool's input schema: a property matchCase is --match-case; objects and arrays take JSON. --input passes the whole argument object as JSON (inline, @args.json, or - for stdin), and flags given with it override its fields. folio <command> --help lists them.

Reading never writes: not the file, not identifiers, not a cache.

Changing a document

| Command | Tool | Does | | --------- | ------------------- | ------------------------------------------------------------ | | suggest | suggest_changes | Applies a batch of edit operations as tracked changes | | comment | add_comment | Comments on a block, optionally quoting text in it | | reply | reply_comment | Replies to a comment thread | | resolve | resolve_comment | Resolves or reopens a comment thread | | accept | resolve_changes | Accepts tracked changes: --id <id> (repeatable) or --all | | reject | resolve_changes | Rejects tracked changes: --id <id> (repeatable) or --all | | compare | compare_documents | Diffs two files; with -o, writes their redline |

folio suggest contract.docx --input @ops.json --in-place --expect-version 9f2c…
folio comment contract.docx --block-id 1A2B3C4D --text "Confirm the rate." -o reviewed.docx --expect-version 9f2c…
folio accept contract.docx --all --in-place --expect-version 51ab…
folio compare signed.docx draft.docx -o redline.docx --expect-version 07de…

suggest --input takes the operation batch: { "operations": [...] }, or the bare array. The operation types and fields are those of suggest_changes in @stll/folio-agents (folio suggest --help lists them); every operation's targets come from a read of the same fileVersion. Edits are tracked changes; --direct edits the text instead and must be asked for.

Two more operations are expanded against the file before the batch applies: { "type": "replaceAll", "find", "replace", "matchCase"?, "wholeWord"? } replaces every match in the body, tables included, keeping each run's formatting (refused when nothing matches); and { "type": "addComment", "comment", "blockId"?, "quote"? } comments on a block, or with only quote on the one block containing that text (ambiguous_target when several do).

Every change commits, so there is no separate save:

  • Destination. Exactly one of --in-place or -o <path>, and only a .docx whose name does not start with a dot and that is not inside a .folio directory (invalid_destination). -o never modifies the input and refuses a path that is the input (or, for compare, the revised file) under another name: symlinks and hard links are resolved and files are compared by device and inode. -o refuses an existing file unless --overwrite is given with --expect-destination-version <fileVersion> of the file being replaced.
  • Preconditions. A change needs --expect-version <fileVersion> from the read it was based on, and refuses (stale_version, exit 10) a file that changed since; --no-expect-version skips the check and must be given explicitly. Under the write lease the file is hashed again right before the commit, so a change made during the edit is refused too. A batch lands whole or not at all: a stale target (stale_target, exit 10), an ambiguous find (ambiguous_target, exit 2), or any other refused operation writes nothing, with the per-operation reasons in error.details.
  • Atomic write. The new package is staged beside the destination, checked (it reopens, every changed XML part is well formed, every relationship id a changed part uses resolves, every internal target a changed .rels part names exists), flushed to disk, and renamed over the destination. Whatever a write replaces (in place, or with --overwrite) is first copied to .folio/backups/<document name>/<fileVersion>.docx, flushed with its directory; the newest 20 per document are kept. A file with more than one hard link is not written in place.
  • Minimal saves. A change is written by patching the edited paragraphs into the original package, leaving every other part's content as it was. When that is not possible (a paragraph added or removed, styles or section properties changed) the command refuses with repack_required rather than silently rewriting the whole package; --allow-repack permits it. The receipt reports saveStrategy (selective, full-repack with a repackReason, or redline) and changedParts. The promise is part content, not identical ZIP bytes.
  • Write lease. A write holds .<name>.docx.folio-lock beside the file (pid, host, expiry, and a random token), created complete or not at all. An editor holding it with unsaved edits is asked to save and release first (see Saving from an editor); any other holder makes a write fail with locked (exit 10) unless --force. Just before journaling and again before the rename, a write checks that the lock still carries its token, so a writer whose lease was taken over stops without writing. Reads ignore the lease. A lease whose process has exited, or that expired, is replaced; a lock that cannot be parsed counts as held until it is older than a lease.
  • Sidecar files. The lock, the stage, the journal, and backups are never read or written through a symlink or a file with more than one hard link (unsafe_path, exit 8). .folio and its subdirectories are created with mode 0700; an existing .folio that is a symlink or not a directory is refused, not followed.
  • Author and time. The author is --author, else FOLIO_AUTHOR, else git user.name; with none, the command refuses. Every revision, comment, and reply of one transaction carries one UTC timestamp, --date to fix it for reproducible output. (compare stamps its redline with the current time.)

Saving from an editor

folio save <file> --from <saved.docx> --expect-version <fileVersion> commits a whole package a live editor serialized, with the same lease, recovery, checks, backup, and journal as a tool call (tool: "editor_save", with --surface and --save-strategy recorded). Unlike the tool commands it may rewrite every part, so it is not an MCP tool. --owner names the lease holder.

An editor with unsaved edits holds the lease long-lived (acceptsFlush, 30 s, renewed every 10 s) and watches for .<name>.docx.folio-flush-<id> requests. A write that finds it writes a request and waits up to --flush-wait ms (default 5000) for the editor to save under its lease (--lease-token) and release; the write then runs on the saved version. An agent tool's --expect-version from before the flush is accepted when the journal shows the editor's save from exactly that version and every block it targets is a package w14:paraId in both versions; its text-hash preconditions decide the rest. A call targeting a text-derived id (blockIdSource: synthetic), or no block, is refused with stale_version and re-reads. An editor that does not release in time is treated by the ordinary lease rules. @stll/folio-cli/editor-lease exports the editor side.

Journal, retries, and recovery

Each committed transaction appends a line to .folio/journal.jsonl beside the destination (or --journal <path>): its txId, fromVersion, toVersion, author, time, the operations, their receipts, and the command's receipt.

--tx-id <key> makes a change idempotent: running a committed transaction again returns its original receipt with status: "replayed" and writes nothing; reusing the key for a different request is refused (transaction_conflict).

The journal line is written after the staged package validates and before the rename. If a process dies between the two, the next write to that file completes the rename only when the line names that file, the stage is a regular single-link file holding the recorded version, it still parses as a package, and the file is still what the transaction replaced; any other stage is discarded (unlinked, never followed). Recovery is reported under recovered, and the write's own version check runs against the file as recovery left it.

Known limits

A process on the same machine that can rename directories inside the document's directory while a write runs can still race it: names are resolved, then re-checked (the handle's device and inode against the checked path, and the directory's real path just before the rename), but not held open across the whole transaction. The tool caller cannot exploit this; it matters only against another local actor with write access to the directory.

Rendering and preview

folio render contract.docx -o contract.pdf
folio render contract.docx -o page-3.png --page 3
folio serve contract.docx

folio render lays the document out with folio's own layout engine and paints it with folio's painters, with no word processor involved: .pdf from the PDF backend (every page, or --page n), .html from the DOM backend, and .png by screenshotting that page in headless Chromium (page 1 unless --page, --scale pixels per CSS pixel, default 2). Text is measured and painted with the metric-compatible @fontsource faces the CLI installs. PNG needs playwright-core and its Chromium, which the CLI does not install:

npm install playwright-core && npx playwright-core install chromium

-o refuses an existing file unless --overwrite; the document is never changed.

folio serve prints a URL and serves a read-only preview of the document on 127.0.0.1 until interrupted. The page re-renders when the file's version changes: it watches the file and its .folio/journal.jsonl, so a change from folio suggest, the MCP server, or any other program appears on its own. The URL carries a random token; requests without it, with a Host other than the loopback address, or with a method other than GET are refused. The server never writes, not even a lock.

Output

Every command prints one envelope:

{ "ok": true, "data": { "path": "/abs/contract.docx", "fileVersion": "9f2c…", "result": {} } }
{ "ok": false, "error": { "code": "stale_version", "message": "…", "hint": "Re-read the document…" } }

--output json (the default when stdout is not a terminal) prints the envelope on stdout for success and failure alike. --output text (the default on a terminal) prints a readable rendering, and failures as error: and hint: lines on stderr.

| Exit | Meaning | | ---- | -------------------------------------------------------------------------- | | 0 | success | | 1 | unexpected internal error | | 2 | usage, input, or refused-operation error | | 6 | file, change, or comment not found | | 8 | path outside the allowed roots, or unsafe to write through | | 10 | conflict with current state (stale version, lock held, destination exists) |

Versions and identifiers

fileVersion is the lowercase hex SHA-256 of the file's bytes. --expect-version <sha256> refuses the command (exit 10, stale_version) unless the file still has that version.

A block id is the paragraph's own w14:paraId when it has one (blockIdSource: "package"). A paragraph without one gets an id derived from the file's bytes (blockIdSource: "synthetic"): stable across reads of the same bytes, and written into the file as its w14:paraId by the first change, so a paragraph keeps its id across edits. A signed package is never rewritten for this; its id-less paragraphs keep ids derived from text and position, valid only for the fileVersion they were read at.

read pages with --max-blocks; a page that stops early carries nextCursor, which --cursor continues. A cursor is bound to the version it was issued for, so a cursor from before an edit is refused rather than continuing on different content.

Offsets in range handles (startOffset, endOffset) are UTF-16 code-unit indices into the block text that read returns, the indexing JavaScript strings use. A consumer that counts Unicode code points converts with Array.from(text.slice(0, offset)).length.

MCP server

folio mcp serves the same tools over the Model Context Protocol on stdio: the protocol owns stdout, diagnostics go to stderr.

folio mcp --root ~/contracts --author "Jane Doe"

Claude Code, in a project's .mcp.json:

{
  "mcpServers": {
    "folio": {
      "command": "npx",
      "args": ["-y", "@stll/folio-cli", "mcp", "--root", "."],
      "env": { "FOLIO_AUTHOR": "Jane Doe" }
    }
  }
}

Codex, in ~/.codex/config.toml:

[mcp_servers.folio]
command = "npx"
args = ["-y", "@stll/folio-cli", "mcp", "--root", "/path/to/contracts"]
env = { FOLIO_AUTHOR = "Jane Doe" }

A model is sent the tool list on every turn, so the server lists only the frequent tools, with compact schemas: read_document, find_text, read_comments, read_changes, suggest_changes, and add_comment. The rest (get_document_outline, read_section, list_stories, read_story, reply_comment, resolve_comment, resolve_changes, and compare_documents) are reached through list_capabilities, describe_capability (a tool's parameter outline, short guidance and an example, listed tools included; detail: "full" for its full input schema), and invoke_capability ({ capability, input, validate_only }). Every tool is also callable by name. Each takes its folio-agents arguments plus a file envelope:

| Field | Meaning | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | path | The .docx, absolute or relative to the first root | | fileVersion | Required on every change (including compare_documents with a destination): the version the caller read; optional on reads | | destination | Write the result to this .docx instead of changing path in place (not a dotfile, not inside .folio) | | overwrite | Let destination replace an existing file; needs expectedDestinationVersion | | expectedDestinationVersion | The version of the file destination replaces; it is backed up first | | txId | Idempotency key, as --tx-id | | allowRepack | As --allow-repack | | mode | suggest_changes only: tracked (default) or direct |

Every path, including destination and compare_documents' revisedPath, must resolve through symlinks inside an allowed root (--root, repeatable, default the current directory), or the call is refused with outside_root. The author comes from --author, FOLIO_AUTHOR, or git user.name when the server starts; without one, reads work and changes are refused. The server never takes over another holder's write lease. Tools that write are annotated destructiveHint: true; there is no way to skip the version check.

One call returns at most 200 read_document blocks by default (up to 1,000 with maxBlocks), 100 find_text matches, and 256 KiB; a longer read pages with nextCursor, and a result that cannot be paged is refused with too_large and a hint to narrow it.

Results carry what the next call needs. read_document returns [id] text lines, a table row as | [id] cell | [id] cell | (formatting: true for each block's fields), a write returns the new fileVersion plus what it produced (applied, replaced, commentIds, ...), and block ids stay valid across writes, so a successful write needs no verification read. A failure is { "error": { code, message, hint, retryable } } with isError set. Arguments are read leniently ("true", "20", an enum value in another case) with an Input read: note; an unknown or ambiguous argument is refused with validation_error, and overwrite and allowRepack count only as JSON true.

Resources: folio://about (these rules and the roots) and folio://schema/operations (the JSON Schema of an operation batch).

Untrusted documents

Reads return document text verbatim. Text from an untrusted .docx can carry instructions aimed at a model that reads it; treat it as data.