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

@mudlor/muddle

v0.1.0

Published

A local-first, collaborative block-based knowledge graph built on Automerge. Access and sync notes programmatically with real-time collaboration.

Downloads

59

Readme

Muddle

This is an example local-first app using Automerge.

It demonstrates:

  • Modeling data as Automerge Documents
  • Managing Documents with an Automerge Repo:
    • Storing Documents in a client-side IndexedDb
    • Synchronizing Documents over Web Sockets
  • Working with Automerge in React:
    • Using a RepoContext to expose a repo to UI components
    • Reading & updating documents with the useDocument hook

Installation

Clone the project, install its dependencies, and run yarn dev to start the local dev server.

$ git clone https://github.com/yourusername/muddle.git
# Cloning into muddle...
$ cd muddle
$ pnpm i
# Installing project dependencies...
$ pnpm dev
# Starting Vite dev server...

Navigate to http://localhost:5173 to see the app running.

You'll notice the URL change to append a hash with an Automerge document ID, e.g.:

http://localhost:5173/#automerge:8SEjaEBFDZr5n4HzGQ312TWfhoq

Open the same URL (including the document ID) in another tab or another browser to see each client's changes synchronize with all other active clients.

Architecture Overview

This application is built as a Roam Research-style note-taking app with nested blocks and real-time collaboration.

Document Types and Components

1. NoteDocument (Data Type)

  • Location: src/components/Block.tsx
  • Purpose: The core data structure for all documents in the system
  • Structure:
    interface NoteDocument {
      type: "block";
      block: Block;
    }
      
    interface Block {
      id: string;
      content: string;
      icon?: string;
      children: AutomergeUrl[];  // References to child blocks
      metadata: {
        created: string;
        modified: string;
        author?: string;
      };
    }
  • Key Design: Each block is its own Automerge document, enabling fine-grained collaboration and efficient syncing

2. UniversalDocument (Component)

  • Location: src/components/UniversalDocument.tsx
  • Purpose: The main React component for rendering any document
  • Features:
    • Handles all document types (unified to use NoteDocument)
    • Manages starring/bookmarking (adds blocks to home page)
    • Handles block creation, deletion, indenting, and unindenting
    • Renders navigation breadcrumbs
    • Supports inline document title editing
    • Handles migration from old document formats
  • Status: Active and recommended - use this for all document rendering

3. RootDocument (Type Alias)

  • Location: src/rootDoc.ts
  • Purpose: Type alias for the root/home document
  • Definition: export type RootDocument = NoteDocument;
  • Note: The root document uses the same structure as all other documents for consistency

4. BlockComponent

  • Location: src/components/BlockComponent.tsx
  • Purpose: Renders individual blocks within documents
  • Features:
    • Roam-style keyboard navigation (Tab/Shift+Tab for indent/unindent)
    • Bullet points that navigate to block as standalone document
    • Expand/collapse for child blocks
    • Rich text editing with auto-expanding textareas
    • Parent references passed via props (not stored in data model)

5. DocumentList (Component)

  • Location: src/components/DocumentList.tsx
  • Purpose: Renders a collapsible list of child documents
  • Usage: Used in the home page to show starred/bookmarked notes

6. DocumentTitleHeader (Component)

  • Location: src/components/DocumentTitleHeader.tsx
  • Purpose: Renders the document title in the app header
  • Features: Shows editable title when viewing root document

Key Architectural Decisions

  1. Document-per-block: Each block is its own Automerge document, allowing:

    • Fine-grained sync (only changed blocks need syncing)
    • Block reuse across documents
    • Efficient collaboration on large documents
  2. No Parent References: Blocks don't store references to their parents. Parent-child relationships are:

    • Stored only as children arrays
    • Parent context passed via React props
    • This prevents inconsistencies and circular references
  3. Unified Document Type: All documents (including root) use the same NoteDocument structure, simplifying the codebase

  4. Navigation-based Breadcrumbs: Breadcrumbs show the actual navigation path taken, not document hierarchy

JavaScript SDK

Muddle provides a JavaScript SDK for third-party applications to access and interact with the block graph programmatically. The SDK mirrors the UI's data model and provides a clean API for reading, writing, and subscribing to changes.

Quick Start

The easiest way to load a Muddle block in your application is to right-click on any block bullet and select "Copy JS Code". This copies a ready-to-use code snippet that connects to the sync server and loads that specific block.

Installation

npm install muddle-notes
# or
pnpm add muddle-notes

Basic Usage - Loading and Modifying a Block

import { connectBlock } from 'muddle-notes/client';

async function main() {
  // Connect to the default sync server
  const client = await connectBlock();

  // Load a block by its Automerge URL
  const block = await client.getBlock('automerge:abc123xyz');

  // Read block data
  const content = await block.getContent();
  const icon = await block.getIcon();
  console.log(`${icon || '•'} ${content}`);

  // Get children
  const children = await block.getChildren();
  for (const child of children) {
    console.log(`  - ${await child.getContent()}`);
  }

  // Modify the block
  await block.updateContent('Updated content!');
  await block.updateIcon('🚀');

  // Add a new child
  const newChild = await client.createBlock({ content: 'New child block' });
  await block.addChild(newChild);

  // Subscribe to real-time changes (from other users)
  block.onChange((data) => {
    console.log('Block updated:', data.content);
  });
}

main();

Full Client (with Root Document)

For applications that need access to the full document tree including address book, favorites, and history:

import { connect } from 'muddle-notes/client';

async function main() {
  // Connect with a root document URL
  const client = await connect('automerge:rootDocUrl', {
    syncServer: 'wss://sync.automerge.org'
  });

  // Get the home section
  const home = await client.getHome();

  // Create a new block
  const newBlock = await client.createBlock({
    content: 'Hello from the SDK!',
    icon: '🚀'
  });

  // Add it as a child of home
  await home.addChild(newBlock);

  // Update content
  await newBlock.updateContent('Updated content');

  // Search for blocks
  const results = await client.search('hello');
  for (const result of results) {
    console.log(`Found: ${result.content} at depth ${result.depth}`);
  }
}

API Reference

Connection Functions

connectBlock(options?)

Creates a lightweight client for accessing blocks with read/write capabilities.

interface ConnectOptions {
  syncServer?: string;        // Default: 'wss://sync.automerge.org'
  enableStorage?: boolean;    // Default: true in browser
  enableBroadcastChannel?: boolean; // Default: true in browser
}

// Returns:
interface BlockClient {
  getBlock(url: AutomergeUrl): Promise<BlockHandle>;
  createBlock(props?: { content?: string; icon?: string }): Promise<BlockHandle>;
  disconnect(): void;
}
connect(rootUrl, options?)

Creates a full MuddleClient with access to address book, favorites, and history.

BlockHandle Methods

| Method | Description | |--------|-------------| | getContent() | Get the block's text content | | getIcon() | Get the block's emoji icon | | getMetadata() | Get created/modified timestamps | | getChildren() | Get all child blocks as BlockHandles | | getChild(index) | Get a specific child by index | | onChange(callback) | Subscribe to changes (returns unsubscribe function) | | toURL() | Get the Automerge URL | | asReadOnly() | Create a read-only view |

Write Methods (not available on read-only handles):

| Method | Description | |--------|-------------| | updateContent(text) | Update the block's content | | updateIcon(emoji) | Update the block's icon | | update({ content, icon }) | Atomic multi-field update | | addChild(block, index?) | Add a child block | | removeChild(index) | Remove child at index | | moveChild(from, to) | Reorder children |

Serialization Methods:

| Method | Description | |--------|-------------| | toMarkdown(options?) | Convert block and children to nested markdown |

toMarkdown(options?)

Converts a block and all its descendants to markdown, with children rendered as nested bullet points:

const block = await client.getBlock('automerge:abc123');
const markdown = await block.toMarkdown();

// Output:
// 🌟 Parent content
//   - Child 1 content
//     - Grandchild content
//   - Child 2 content

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | indent | string | ' ' | Indentation per level | | includeIcons | boolean | true | Include emoji icons | | maxDepth | number | -1 | Max depth (-1 = unlimited) | | preserveWikiLinks | boolean | true | Keep [[WikiLinks]] or strip to plain text |

// Strip WikiLinks and limit depth
const markdown = await block.toMarkdown({
  preserveWikiLinks: false,
  maxDepth: 2,
  indent: '    '
});

MuddleClient Methods

| Method | Description | |--------|-------------| | getRoot() | Get the root block | | getHome() | Get the Home section | | getBlockByURL(url) | Load any block by URL | | createBlock(props?) | Create a new block | | search(query, options?) | Search for blocks by content | | walkBlocks(options?) | Async generator yielding all blocks | | getAllBlocks(options?) | Get all blocks as an array | | getAddressBook() | Get the Address Book section | | getFavorites() | Get the Favorites section | | lookupByName(name) | Find a block by its address book name |

Block Discovery

Discover and list all blocks without needing specific URLs:

import { connect } from 'muddle-notes/client';

const client = await connect('automerge:rootUrl');

// Iterate over all blocks (async generator)
for await (const { block, depth, path } of client.walkBlocks()) {
  const content = await block.getContent();
  console.log('  '.repeat(depth) + content);
}

// Or get all blocks as an array
const allBlocks = await client.getAllBlocks({ maxDepth: 3 });
console.log(`Found ${allBlocks.length} blocks`);

// Walk from any starting block
const home = await client.getHome();
for await (const { block, depth } of home.walkDescendants()) {
  console.log('  '.repeat(depth) + await block.getContent());
}

BlockHandle Discovery Methods:

| Method | Description | |--------|-------------| | walkDescendants(options?) | Async generator yielding all descendants | | getAllDescendants(options?) | Get all descendants as an array |

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | maxDepth | number | -1 | Max depth (-1 = unlimited) | | maxBlocks | number | -1 | Max blocks to return (-1 = unlimited) | | includeSelf | boolean | true | Include starting block in results |

Code Snippet Generation

The SDK includes utilities to generate code snippets:

import { generateCodeSnippet, generateHtmlSnippet } from 'muddle-notes/client';

// Generate JavaScript code
const jsCode = generateCodeSnippet('automerge:abc123', {
  typescript: false,
  esm: true,
  includeChangeHandler: true
});

// Generate a standalone HTML page
const html = generateHtmlSnippet('automerge:abc123', {
  title: 'My Block'
});

AgentSkills Integration

Export notes as AgentSkills for use with LLM coding assistants like Claude Code, Cursor, VS Code Copilot, and others.

From the UI

Right-click any block and select "Copy as Skill" to copy an AgentSkills-compatible SKILL.md to your clipboard.

Programmatically

import { connectBlock, generateSkillSnippet } from 'muddle-notes/client';

const client = await connectBlock();
const block = await client.getBlock('automerge:abc123');

// Convert to nested markdown
const markdown = await block.toMarkdown();

// Generate AgentSkills SKILL.md format
const skill = generateSkillSnippet('automerge:abc123', markdown, {
  name: 'project-architecture',
  description: 'Context about project structure. Use when asked about architecture decisions.',
});

console.log(skill);

Output:

---
name: project-architecture
description: Context about project structure. Use when asked about architecture decisions.
metadata:
  source: "muddle"
  blockUrl: "automerge:abc123"
---

# Note Content

🏗️ Project Architecture
  - Frontend: React + TypeScript
  - Backend: Node.js + Express
  - Database: PostgreSQL with Prisma

---

## Programmatic Access

This note can be accessed programmatically...

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | auto | Skill name (auto-generated from content) | | description | string | auto | When to use this skill | | syncServer | string | default | Custom sync server URL | | includeClientCode | boolean | true | Include JS client examples | | license | string | - | License for the skill | | metadata | object | - | Additional metadata key-values |

Sync Servers

By default, the SDK connects to wss://sync.automerge.org. You can specify a custom server:

const client = await connectReadOnly({
  syncServer: 'wss://your-server.example.com'
});

Browser vs Node.js

The SDK works in both environments:

  • Browser: Uses IndexedDB for storage and BroadcastChannel for tab sync
  • Node.js: In-memory only by default (pass enableStorage: false)

TypeScript Support

The SDK is written in TypeScript and includes full type definitions:

import {
  BlockHandle,
  MuddleClient,
  Block,
  AutomergeUrl
} from 'muddle-notes/client';

CLI for AI Agents

A command-line interface for AI agents to interact with Muddle documents:

# Read a document and its children
npm run muddle read <url>
npm run muddle read chat                    # Use alias

# Write a new block as child
npm run muddle write <url> "Your message"
npm run muddle write chat "Hello from Claude!"

# Show full tree structure
npm run muddle tree <url>

# Watch for real-time changes
npm run muddle watch <url>

# Export as AgentSkill
npm run muddle skill <url>

Built-in aliases:

  • chatautomerge:c6w8LMRV3gb8idvcNwTnmxjBp88

Example session:

$ npm run muddle read chat
📝 Muddle Skill
  - • Does this work?
    - • yes
  - 🐙 Otto here, writing directly via Automerge
  - 🤖 Claude Code checking in!

$ npm run muddle write chat "New message from AI agent"
✓ Added block: automerge:xyz123
✓ Synced to server

Publishing to npm

To publish a new version of the muddle-notes package:

# 1. Clone and install dependencies
git clone https://github.com/yourusername/muddle.git
cd muddle
npm install

# 2. Run tests to ensure everything works
npm test -- --run

# 3. Build the client library
npm run build:client

# 4. Log in to npm (if not already)
npm login

# 5. Bump version (patch/minor/major)
npm version patch   # 0.1.0 -> 0.1.1
# or: npm version minor  # 0.1.0 -> 0.2.0
# or: npm version major  # 0.1.0 -> 1.0.0

# 6. Publish to npm
npm publish

# 7. Push the version tag to git
git push && git push --tags

Pre-publish Checklist

  • [ ] All tests pass: npm test -- --run
  • [ ] TypeScript compiles: npm run type-check
  • [ ] Client builds: npm run build:client
  • [ ] Package contents look correct: npm pack --dry-run

What Gets Published

Only the client SDK is published (not the full app):

muddle-notes/
├── dist/client/
│   ├── index.js        # Main entry point
│   ├── index.d.ts      # TypeScript definitions
│   └── chunk-*.js      # Code-split modules
├── README.md
└── package.json