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-agents

v0.5.0

Published

Framework-neutral LLM tool layer over folio's ai-edits engine: function-calling tools so an agent can read and mutate .docx documents through @stll/folio-core.

Downloads

3,197

Readme

@stll/folio-agents

A framework-neutral (React-free, DOM-free) LLM tool layer over @stll/folio-core's AI-edits engine: function-calling tool definitions and an executor so a model can read and mutate a .docx document, with every mutation landing as a tracked change or comment pending human review.

Part of stella, an open-source legal workspace.

Install

bun add @stll/folio-agents

@stll/folio-core is installed automatically as a dependency.

Tools

| Tool | What it does | | ---------------------- | ---------------------------------------------------------------------- | | read_document | Read the document body as { blockId, kind, text } blocks | | get_document_outline | Read heading hierarchy and stable section handles | | read_section | Read a bounded logical section, with block cursor pagination | | list_stories | List main, header, footer, footnote, and endnote story handles | | read_story | Read one story by its typed handle | | find_text | Search by document, section, real page, or story scope | | read_comments | Read comment threads (author, text, resolved, anchored block, replies) | | read_changes | Read pending tracked changes (insertions/deletions) awaiting review | | add_comment | Attach a comment to a block, optionally quoting specific text | | suggest_changes | Propose block or stable-range edits as tracked changes | | reply_comment | Reply to a comment thread | | resolve_comment | Resolve or reopen a comment thread | | read_page | Read a page's plain text (live editor only) | | read_selection | Read the current text selection (live editor only) | | scroll_to_block | Scroll the live editor to a block (live editor only) | | show_in_document | Reveal a stable block or exact text range (live editor only) |

Block ids and comment ids always come from a prior tool call (read_document, find_text, read_comments) within the same conversation — never guess them. suggest_changes reports a plain-language reason when an operation is skipped (e.g. the block changed since it was last read), so the model can re-read and retry. Successful mutation results include receipts that identify affected blocks, ranges, insertions, and created comments.

For document questions, start with get_document_outline, then call read_section or scoped find_text. This keeps unrelated contract text out of model context. Section handles use Folio block identities, heading depth, and a text hash, so structurally changed, renamed, or deleted headings fail stale instead of resolving to the wrong content. Page scopes and page numbers use Folio's live layout; they are never approximated from character counts and therefore require a live, paginated editor.

Untrusted documents

read_document, read_section, read_story, read_page, read_comments, read_changes, and find_text return document content verbatim. If a .docx comes from an untrusted party, its text can carry prompt-injection payloads straight into the model's context — treat any document-derived tool result as untrusted model input, the same way you would treat a fetched web page. Mutations stay safe by design regardless: suggest_changes and add_comment land as tracked changes or comments pending human review, so an injected instruction can propose an edit but cannot silently apply one.

Headless quickstart

import { FolioDocxReviewer } from "@stll/folio-core/server";
import {
  createReviewerBridge,
  executeFolioToolCall,
  getFolioToolDefinitions,
  toAnthropicTools,
} from "@stll/folio-agents";

const reviewer = await FolioDocxReviewer.fromBuffer(docxBuffer, { author: "AI" });
const bridge = createReviewerBridge(reviewer);
const tools = toAnthropicTools(getFolioToolDefinitions());

// Inside your tool-use loop, for each tool_use block the model emits:
const result = executeFolioToolCall(toolName, toolInput, bridge);
// result: { ok: true, result } | { ok: false, error } — feed either back to the model.

const reviewedBuffer = await reviewer.toBuffer();

Live-editor quickstart

import { createEditorRefBridge, executeFolioToolCall } from "@stll/folio-agents";

// `docxEditorRef` is a DocxEditorRef from @stll/folio-react (or any object
// structurally matching FolioAgentEditorRefLike).
const bridge = createEditorRefBridge({
  ref: docxEditorRef.current,
  author: "AI",
  getComments: () => comments,
  setComments: (next) => setComments(next),
});

const result = executeFolioToolCall("suggest_changes", { operations: [...] }, bridge);

On a DocxEditorRef that implements the read surface (getTrackedChanges, getCommentAnchors, getSelectionText, getPageText, getTargetPage, and showInDocument), the editor-ref bridge has full parity with the headless one: read_changes returns real tracked changes, comment entries carry a resolved blockId / quote, and read_page / read_selection work against the live view. Against an older ref that predates those methods, the bridge degrades per-member: read_changes returns [], comment entries fall back to blockId: null / quote: "", and read_page / read_selection report an unsupported-capability error — see src/bridges/editor-ref.ts for the exact fallback per method.

Host-managed review queue

A host with its own review-queue UX (its own place to store proposed edits pending approval, distinct from folio's tracked-changes redlines) can validate a model's suggest_changes / add_comment tool-call arguments with the same canonical rules executeFolioToolCall uses, without applying them through a bridge at all:

import { parseSuggestChangesInput } from "@stll/folio-agents";

const parsed = parseSuggestChangesInput(toolInput);
if (!parsed.ok) {
  // Feed `parsed.error` back to the model as the tool result, same as
  // executeFolioToolCall would.
} else {
  // `parsed.operations` is FolioAIEditOperation[] — route it into your own
  // review queue instead of bridge.applyDocumentOperations(...).
  reviewQueue.enqueue(parsed.operations);
}

parseAddCommentInput is the equivalent for add_comment, returning { ok: true; operation } on success.

Summarizing changes

Two different questions come up under "what changed":

1. Pending tracked changes in one document — what a human reviewer would see as redlines right now. Use the read_changes tool (or reviewer.getChanges() directly) and hand the insertions/deletions to the model:

const changes = reviewer.getChanges();
const prompt = `Summarize these pending edits for a reviewer:\n${changes
  .map((c) => `${c.type === "insertion" ? "+" : "-"} [${c.blockId}] ${c.text}`)
  .join("\n")}`;

2. Between two saved versions — what changed across two .docx buffers, independent of whether either one has any tracked changes at all. Use compareDocxVersions + formatVersionDiffForLLM:

import { compareDocxVersions, formatVersionDiffForLLM } from "@stll/folio-agents";

const diff = await compareDocxVersions(previousVersionBuffer, currentVersionBuffer);
const prompt = `Summarize what changed between these two document versions:\n${formatVersionDiffForLLM(diff)}`;
// -> feed `prompt` to your model as a normal user/system message.

Both recipes compare the AS-ACCEPTED view of a document: any tracked changes already pending in a buffer count as already applied before the comparison runs (compareDocxVersions parses each buffer through the same FolioDocxReviewer snapshot read_document uses). Diffing two versions that each have their own uncommitted redlines still produces a clean, readable diff instead of raw markup noise.

compareDocxVersions ships as a plain async function, not a tool definition: a model can describe a tool call, but it can't attach two document buffers to one — buffers aren't JSON-serializable tool arguments a model could produce. The natural shape is a host-side tool keyed by version identifiers instead (e.g. a server tool the model calls with two stored version ids, which your backend resolves to buffers, diffs, and returns the formatted text for).

TanStack AI

TanStack AI's toolDefinition accepts a raw JSON Schema object as inputSchema, so the definitions plug in without any wrapper:

import { toolDefinition } from "@tanstack/ai";
import { getFolioToolDefinitions } from "@stll/folio-agents";

const defs = getFolioToolDefinitions().map((def) =>
  toolDefinition({
    name: def.name,
    description: def.description,
    inputSchema: def.inputSchema,
  }),
);
// Client-executed tools: run executeFolioToolCall(name, args, bridge) where the
// live editor lives and report the payload back via your chat client's
// addToolResult; server-executed tools: chain .server((args) => ...) instead.

The schemas stay within a conservative JSON Schema subset (type: "object", properties, required, enum, additionalProperties: false, plain arrays). Some providers (e.g. Gemini's OpenAPI-3.0 subset) reject less common keywords; if your stack projects tool schemas through a provider-safe filter, these definitions pass through it unchanged.

Vercel AI SDK

This package ships no ai dependency; map its tool definitions with the AI SDK's own jsonSchema() / tool() helpers:

import { jsonSchema, tool } from "ai";
import { executeFolioToolCall, getFolioToolDefinitions } from "@stll/folio-agents";

const tools = Object.fromEntries(
  getFolioToolDefinitions().map((def) => [
    def.name,
    tool({
      description: def.description,
      inputSchema: jsonSchema(def.inputSchema),
      execute: async (input) => executeFolioToolCall(def.name, input, bridge),
    }),
  ]),
);

Acknowledgements

folio began as a private fork of Eigenpal's docx-editor. The original license and copyright are preserved in NOTICE.md.

License

Apache-2.0