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

@shashimadushan/docx-editor-agent

v0.2.0

Published

AI agent for @shashimadushan/docx-editor-editor. Tool-calling agent that can read, write, insert, delete, format, and search document content via any LLM (OpenAI, Anthropic, or custom).

Downloads

87

Readme

@shashimadushan/docx-editor-agent

AI agent for @shashimadushan/docx-editor-editor. Tool-calling agent that can read, write, insert, delete, format, and search document content via any LLM.

Install

npm install @shashimadushan/docx-editor-agent @shashimadushan/docx-editor-editor react react-dom

Quick start (React)

import * as React from 'react';
import { ReactDocxEditor, type DocxEditor } from '@shashimadushan/docx-editor-editor/react';
import '@shashimadushan/docx-editor-editor/style.css';
import { BackendAdapter } from '@shashimadushan/docx-editor-agent';
import { useDocxAgent } from '@shashimadushan/docx-editor-agent/react';

function App() {
  const [editor, setEditor] = React.useState<DocxEditor | null>(null);
  // Memoize the adapter — useDocxAgent rebuilds its DocxAgent (and resets
  // the conversation) whenever the adapter *reference* changes, so passing
  // `new BackendAdapter(...)` inline here would reset on every render.
  // BackendAdapter talks to YOUR server (see the "BackendAdapter" section
  // below) — no provider API key ever reaches the browser.
  const adapter = React.useMemo(() => new BackendAdapter({ baseUrl: '/api/agent' }), []);
  const { isRunning, log, run, reset, error } = useDocxAgent({ adapter }, editor);

  return (
    <>
      <ReactDocxEditor onReady={setEditor} />
      <button onClick={() => run('add heading called Summary')} disabled={isRunning || !editor}>
        Add heading
      </button>
      <pre>{JSON.stringify(log, null, 2)}</pre>
    </>
  );
}

Quick start (headless)

import { DocxEditor } from '@shashimadushan/docx-editor-editor';
import { DocxAgent, OpenAIAdapter } from '@shashimadushan/docx-editor-agent';
import '@shashimadushan/docx-editor-editor/style.css';

const editor = new DocxEditor({ element: document.getElementById('host')! });

const agent = new DocxAgent({
  editor,
  adapter: new OpenAIAdapter({
    apiKey: process.env.OPENAI_API_KEY!,
    model: 'gpt-4o-mini',
  }),
});

// Run an autonomous task
const reply = await agent.run(
  'Add a heading called "Summary" and a 3-bullet list of key points.',
);
console.log(reply);

// Inspect what happened
console.log(agent.history);

LLM adapters

OpenAIAdapter

Works with OpenAI and any OpenAI-compatible provider (Together AI, Groq, Mistral, OpenRouter, vLLM, LM Studio, Ollama with OpenAI compatibility, etc.):

import { OpenAIAdapter } from '@shashimadushan/docx-editor-agent';

new OpenAIAdapter({
  apiKey: process.env.OPENAI_API_KEY!,
  model: 'gpt-4o-mini',
  // Optional: use any OpenAI-compatible provider
  baseURL: 'https://api.groq.com/openai/v1',
});

AnthropicAdapter

import { AnthropicAdapter } from '@shashimadushan/docx-editor-agent';

new AnthropicAdapter({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  model: 'claude-3-5-sonnet-20241022',
});

BackendAdapter (recommended for anything shipped to users)

OpenAIAdapter/AnthropicAdapter need a provider API key in the browser — fine for a local prototype, not for production. BackendAdapter instead talks to two endpoints on your own server, which holds the real key and makes the provider call for you:

import { BackendAdapter } from '@shashimadushan/docx-editor-agent';

const adapter = new BackendAdapter({
  baseUrl: '/api/agent', // relative or absolute; change anytime via adapter.setBaseUrl(url)
  // headers: () => ({ Authorization: `Bearer ${myAppSessionToken}` }), // auth to YOUR api, not the LLM provider
});

It POSTs the exact same { messages, tools, temperature, maxTokens } body DocxAgent always builds to:

  • POST {baseUrl}/chat — one-shot; your server responds with JSON { message, finishReason, usage? }.
  • POST {baseUrl}/chat/stream — streaming; your server responds text/event-stream with data: lines shaped { type: 'token'|'thinking', text } and a final { type: 'done', message, finishReason, usage? }.

The simplest server implementation reuses this same package's OpenAIAdapter/AnthropicAdapter on the server, where holding a secret key is fine. Don't just forward whatever tools/system message the client sent, though — that puts the browser in charge of the agent's capabilities. Use @shashimadushan/docx-editor-agent/server's handleAgentChat/handleAgentChatStream instead: they build the tool schema + system prompt from this package's own registry (server-authoritative), call the adapter, and shape the response BackendAdapter expects — a route is a few lines:

// app/api/agent/chat/route.ts (Node or Edge runtime; any server framework works the same way)
import { handleAgentChat } from '@shashimadushan/docx-editor-agent/server';
import { OpenAIAdapter } from '@shashimadushan/docx-editor-agent';

const adapter = new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o-mini' });

export async function POST(req: Request) {
  return Response.json(await handleAgentChat(await req.json(), adapter));
}
// app/api/agent/chat/stream/route.ts — same idea, streaming
import { handleAgentChatStream, AGENT_SSE_HEADERS } from '@shashimadushan/docx-editor-agent/server';

export async function POST(req: Request) {
  return new Response(handleAgentChatStream(await req.json(), adapter), { headers: AGENT_SSE_HEADERS });
}

See examples/demo/app/api/agent/chat/route.ts and .../chat/stream/route.ts for the complete reference implementation. handleAgentChat* take an optional third argument, { toolRegistry?, systemPrompt? }, if you need a custom tool set (e.g. legal tools merged in, or some builtins disabled) or a different prompt — pass systemPrompt: null to omit it entirely if you'd rather manage that yourself.

Custom adapter

Implement the LLMAdapter interface:

import type { LLMAdapter, LLMMessage, LLMResponse } from '@shashimadushan/docx-editor-agent';

class MyAdapter implements LLMAdapter {
  async complete({ messages, tools }): Promise<LLMResponse> {
    // Call your LLM...
    return {
      message: {
        role: 'assistant',
        content: 'Done.',
        toolCalls: [
          { id: 'call_1', name: 'insert_heading', arguments: { text: 'Hello', level: 2 } },
        ],
      },
      finishReason: 'tool_calls',
    };
  }
}

Built-in tools (67)

67 tools across 14 categories, each carrying category + risk (read / write / destructive) metadata used by the policy gate (see "Security" below). ⚠ marks a destructive tool. For the full generated listing, call buildToolCategoryGuide(tools) or read skill/references/agent-api.md.

| Category | Tools | |---|---| | Inspection (read) | get_outline, get_word_count, list_blocks, get_block, get_block_range, find_block_by_text, find_target_block, get_selection, get_document, get_block_count | | Batch | batch_insert, batch_format, batch_delete ⚠, batch_reorder | | Insert | insert_paragraph, insert_heading, insert_bullet_list, insert_ordered_list, insert_table, insert_code_block, insert_blockquote, insert_horizontal_rule, append_html | | Insert media | insert_image, insert_link, insert_page_break | | Format | set_bold, set_italic, set_underline, set_strikethrough, set_color, set_alignment, set_heading, set_paragraph | | Style | set_paragraph_style, clear_formatting, set_font_family, set_font_size, set_line_height, set_text_transform | | Image layout | set_image_alignment, set_image_wrap, set_image_size | | Table | get_table_info, set_table_cell, add_table_row, delete_table_row ⚠, add_table_column, delete_table_column ⚠ | | Manipulation | duplicate_block, insert_at_cursor, replace_selection, wrap_selection, set_block_text | | Cursor-relative | get_cursor_block, insert_relative, delete_current_block ⚠ | | Structure | delete_block ⚠, replace_block, move_block | | Search | find_text, replace_text ⚠, clear_document ⚠ | | Page-level | set_watermark, set_header, set_footer | | Navigate | scroll_to_block |

Selected tool parameters:

Insert content (9)

| Tool | Description | Parameters | |---|---|---| | insert_paragraph | Insert a paragraph | text: string, index?: number, bold?: boolean, italic?: boolean | | insert_heading | Insert a heading | text: string, level?: 1-6 (default: 2), index?: number | | insert_bullet_list | Insert a bulleted list | items: string[], index?: number | | insert_ordered_list | Insert a numbered list | items: string[], index?: number | | insert_table | Insert a table | rows?: number (default: 3), cols?: number (default: 3), headers?: string[], data?: string[][], index?: number | | insert_code_block | Insert a code block | code: string, index?: number | | insert_blockquote | Insert a blockquote | text: string, index?: number | | insert_horizontal_rule | Insert an <hr> | index?: number | | append_html | Append arbitrary HTML | html: string |

Insert media (3)

| Tool | Description | Parameters | |---|---|---| | insert_image | Insert an image | src: string (URL or data URI), alt?: string, title?: string, index?: number | | insert_link | Insert a hyperlink | href: string, text: string, index?: number (omit to append a new paragraph) | | insert_page_break | Insert a page break | index?: number |

Format (8)

| Tool | Description | Parameters | |---|---|---| | set_bold | Apply/remove bold | index?: number (omit for all), enabled?: boolean (default: true) | | set_italic | Apply/remove italic | index?: number, enabled?: boolean | | set_underline | Apply/remove underline | index?: number, enabled?: boolean | | set_strikethrough | Apply/remove strikethrough | index?: number, enabled?: boolean | | set_color | Set text color | color: string (CSS color), index?: number | | set_alignment | Set text alignment | alignment: 'left' \| 'center' \| 'right' \| 'justify', index?: number | | set_heading | Convert a block to a heading | index: number, level: 1-6 | | set_paragraph | Convert a block back to a paragraph | index: number |

Style / font (4)

| Tool | Description | Parameters | |---|---|---| | set_paragraph_style | Convert a block to a different style | index: number, style: 'paragraph' \| 'h1'-'h6' \| 'blockquote' \| 'codeBlock' | | clear_formatting | Remove all marks | index?: number (omit for all) | | set_font_family | Set font family | fontFamily: string (CSS), index?: number | | set_font_size | Set font size | fontSize: string (CSS length like "12pt"), index?: number |

Mutate structure (3)

| Tool | Description | Parameters | |---|---|---| | delete_block | Delete a block | index: number | | replace_block | Replace a block with new JSON | index: number, block: object | | move_block | Move a block from one position to another | from: number, to: number |

Search & clear (3)

| Tool | Description | Parameters | |---|---|---| | find_text | Find occurrences of a substring | query: string, caseSensitive?: boolean (default: false) | | replace_text | Replace occurrences | find: string, replace: string, caseSensitive?: boolean, firstOnly?: boolean | | clear_document | Empty the document | — |

Navigate (1)

| Tool | Description | Parameters | |---|---|---| | scroll_to_block | Scroll editor to a block | index: number |


DocxAgent options

interface DocxAgentOptions {
  editor: DocxEditor;              // the editor to operate on
  adapter: LLMAdapter;             // LLM adapter (OpenAI/Anthropic/Backend/custom)
  systemPrompt?: string;           // override the default system prompt
  tools?: ToolRegistry;            // custom tool registry (default: all builtins)
  disableTools?: string[];         // disable specific builtin tools by name
  customTools?: AgentTool[];       // additional tools on top of builtins
  temperature?: number;            // default: 0.2
  maxTokens?: number;              // default: 1024
  maxRounds?: number;              // max tool-call rounds per run() (default: 8)

  // Security / tool safety (all optional):
  disableToolsByRisk?: ('read' | 'write' | 'destructive')[]; // drop risk tiers from schema + execution
  allowedCategories?: ToolCategory[];                        // allow-list categories only
  onConfirmDestructive?: (info: { toolName: string; args: any }) => boolean | Promise<boolean>;
}

Security

The agent applies defense-in-depth without touching the editor's mutation flow. Full details in skill/references/agent-api.md ("Security model") and ARCHITECTURE.md.

  • Tool risk gating — every tool has a risk (read/write/destructive). Pass onConfirmDestructive to confirm before destructive tools run, or disableToolsByRisk / allowedCategories to drop them from both the schema and the executable set (e.g. a read-only reviewer agent).
  • Content sanitizationappend_html/set_header/set_footer sanitize HTML (strip scripts, event handlers, unsafe URLs); insert_image/insert_link reject dangerous URL schemes. Swap in a stricter sanitizer via setHtmlSanitizer.
  • Server request boundsvalidateAgentRequest (from /server) clamps temperature/maxTokens and caps payload size. Auth + rate limiting remain the consuming app's responsibility.

Disable specific tools

new DocxAgent({
  editor,
  adapter,
  disableTools: ['clear_document'],  // prevent the agent from clearing the doc
});

Custom system prompt

new DocxAgent({
  editor,
  adapter,
  systemPrompt: 'You are a strict legal-document editor. Only make changes the user explicitly requests.',
});

Custom tools

import type { AgentTool } from '@shashimadushan/docx-editor-agent';

const translateTool: AgentTool = {
  name: 'translate_document',
  description: 'Translate all text in the document to the target language.',
  parameters: {
    type: 'object',
    properties: {
      language: { type: 'string', description: 'Target language, e.g. "Spanish".' },
    },
    required: ['language'],
  },
  execute: async (params, ctx) => {
    const text = ctx.getText();
    const translated = await myTranslateAPI(text, params.language);
    // ... replace text in document via ctx.setDoc() ...
    return { original: text, translated };
  },
};

new DocxAgent({
  editor,
  adapter,
  customTools: [translateTool],
});

AgentTool interface

interface AgentTool<TParams = any, TResult = any> {
  name: string;                    // unique tool name
  description: string;             // the LLM uses this to decide when to call
  parameters: JSONSchema;          // JSON Schema for params
  execute: (params: TParams, ctx: AgentToolContext) => Promise<TResult> | TResult;
}

interface AgentToolContext {
  editor: TipTapEditor;            // the underlying TipTap editor
  doc: any;                        // current document as JSON
  setDoc(json: any): void;         // replace the whole document
  appendContent(content: string | any): void;  // append HTML or JSON node
  insertAtCursor(content: string | any): void; // insert at cursor
  getText(): string;               // plain text of the document
}

useDocxAgent React hook

const { agent, isRunning, log, run, reset, error } = useDocxAgent(options, editor);

| Return | Type | Description | |---|---|---| | agent | DocxAgent \| null | The agent instance (null until editor is ready) | | isRunning | boolean | True while the agent is processing | | log | LogEntry[] | Conversation log | | run(prompt) | Promise<void> | Send a user prompt to the agent | | reset() | void | Clear conversation history | | error | string \| null | Last error message |

Memoize options.adapter. The hook rebuilds its internal DocxAgent (and clears log/error) whenever editor or options.adapter changes identity — that's what makes swapping providers at runtime (e.g. a dropdown that switches between BackendAdapter/OpenAIAdapter/AnthropicAdapter, or picks up a newly-entered API key or a changed baseUrl) actually take effect instead of silently continuing to run whatever adapter was active on the first render. The tradeoff: passing a fresh adapter instance on every render (adapter: new BackendAdapter({ baseUrl }) inline in JSX) resets the conversation on every render too. Wrap it in React.useMemo(() => new BackendAdapter({ baseUrl }), [baseUrl]), keyed on whatever actually determines the adapter (provider kind, API key/baseUrl, model), same as examples/demo/app/page.tsx does.

LogEntry types

type LogEntry =
  | { id: string; type: 'user'; text: string; ts: number }
  | { id: string; type: 'assistant'; text: string; ts: number }
  | { id: string; type: 'thinking'; text: string; ts: number; streaming: boolean }
  | {
      id: string; // the tool call's id — the matching result updates this same entry
      type: 'tool';
      name: string;
      args: any;
      status: 'running' | 'done' | 'error';
      result?: any;
      summary?: string;
      ts: number;
    };

A tool_call and its tool_result are merged into a single 'tool' entry (matched by call id) rather than two separate log entries — the entry starts as status: 'running' and updates in place once the result comes back. The 'thinking' entry streams in incrementally when the adapter supports it (currently AnthropicAdapter with thinking: true); streaming is true until that round's reasoning is complete.


How the agent loop works

When you call agent.run(prompt):

  1. Send { system prompt, conversation history, tool definitions } to the LLM
  2. If the LLM returns a tool call, execute it and append the result to history
  3. Repeat (up to maxRounds) until the LLM returns a plain text response with no tool calls
  4. Return the final text response
User: "Add a heading called Summary"
  ↓
LLM: tool_call(insert_heading, { text: "Summary", level: 2 })
  ↓
Agent executes insert_heading → returns { insertedAt: 3 }
  ↓
LLM: "Done — I added a 'Summary' heading to the document."
  ↓
return "Done — I added a 'Summary' heading to the document."

Example: full agent UI

function AgentPanel({ editor }: { editor: DocxEditor | null }) {
  const adapter = React.useMemo(() => new BackendAdapter({ baseUrl: '/api/agent' }), []); // see note above — memoize this
  const { isRunning, log, run, reset, error } = useDocxAgent({ adapter }, editor);
  const [prompt, setPrompt] = React.useState('');

  const send = async () => {
    if (!prompt.trim()) return;
    await run(prompt);
    setPrompt('');
  };

  return (
    <div style={{ width: 340, display: 'flex', flexDirection: 'column' }}>
      <div style={{ flex: 1, overflow: 'auto' }}>
        {log.map((entry, i) => (
          <div key={i}>
            {entry.type === 'user' && <p><b>You:</b> {entry.text}</p>}
            {entry.type === 'thinking' && <p><i>Thinking: {entry.text}</i></p>}
            {entry.type === 'assistant' && <p><b>Agent:</b> {entry.text}</p>}
            {entry.type === 'tool' && (
              <pre>→ {entry.name} ({entry.status}): {entry.summary ?? JSON.stringify(entry.args)}</pre>
            )}
          </div>
        ))}
      </div>
      <input
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && send()}
        placeholder="Ask the agent…"
        disabled={isRunning}
      />
      <button onClick={send} disabled={isRunning}>Send</button>
      <button onClick={reset}>Reset</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

License

MIT