npm package: @mudlor/muddle
Description: A local-first, collaborative block-based knowledge graph built on Automerge. Access and sync notes programmatically with real-time collaboration.
Latest version: 0.1.0

# Muddle

This is an example [local-first](https://www.inkandswitch.com/local-first) app using [Automerge](https://automerge.org).

It demonstrates:
- Modeling data as Automerge `Document`s
- 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.

```bash
$ 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**:
  ```typescript
  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

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

### Basic Usage - Loading and Modifying a Block

```javascript
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:

```javascript
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.

```typescript
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:

```javascript
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 |

```javascript
// 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:

```javascript
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:

```javascript
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](https://agentskills.io) 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

```javascript
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:**

```yaml
---
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:

```javascript
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:

```typescript
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:

```bash
# 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:**
```bash
$ 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:

```bash
# 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
```