@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
Maintainers
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
RepoContextto expose a repo to UI components - Reading & updating documents with the
useDocumenthook
- Using a
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
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
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
Unified Document Type: All documents (including root) use the same NoteDocument structure, simplifying the codebase
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-notesBasic 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 contentOptions:
| 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:
chat→automerge: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 serverPublishing 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 --tagsPre-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