Skip to content

vcavallo/zettelkastr

Repository files navigation

Zettlkastr

Convert Markdown/Obsidian notes to Nostr events with backlink support using NIP-54 wiki articles.

Overview

Zettlkastr publishes your zettelkasten notes to the Nostr protocol, preserving the critical feature of bidirectional linking. Each markdown file becomes a NIP-54 wiki article event (kind:30818) with wikilinks converted to Nostr a tags, enabling backlink discovery via relay queries.

Features

  • Parse any directory of .md files (Obsidian vaults or plain markdown)
  • Extract wikilinks: [[Note Name]], [[Note|Display]], [[Note#Heading]]
  • Generate NIP-54 events with proper d-tag normalization
  • Publish to local or remote Nostr relays
  • Query notes and backlinks via CLI
  • Interactive web UI with markdown rendering and network graph
  • Preserve frontmatter (title, description)

Prerequisites

1. Python 3.10+

python --version  # Should be 3.10 or higher

2. strfry (Nostr Relay)

Install strfry for local testing:

# Clone and build
git clone https://github.com/hoytech/strfry.git
cd strfry
git submodule update --init
make setup-golpe
make -j4

# Run relay
./strfry relay

strfry will start on ws://localhost:7777 by default.

3. nak (for key generation)

Install nak for Nostr key management:

# With Go
go install github.com/fiatjaf/nak@latest

# Or download binary from releases

Generate a keypair:

nak key generate
# Outputs:
# privkey: <64-char-hex>
# pubkey: <64-char-hex>

Installation

From Source

# Clone repository
git clone https://github.com/yourusername/zettlkastr.git
cd zettlkastr

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Install in development mode
pip install -e .

Configuration

Create a .env file in the project root:

cp .env.example .env

Edit .env:

# Your Nostr private key (from nak key generate)
NOSTR_PRIVATE_KEY=your_64_char_hex_private_key

# Relay URL (default: local strfry)
RELAY_URL=ws://localhost:7777

# Optional: default vault path
VAULT_PATH=/path/to/your/notes

Important: Never commit .env with real keys! It's in .gitignore by default.

Usage

Check Configuration

zettlkastr info

This verifies:

  • .env file exists
  • Private key is valid
  • Relay is accessible

Import a Vault

# Using .env configuration
zettlkastr import-vault /path/to/notes

# Or specify key and relay explicitly
zettlkastr import-vault /path/to/notes --key <privkey> --relay ws://localhost:7777

# Dry run (parse and generate events without publishing)
zettlkastr import-vault /path/to/notes --dry-run

Output:

Using public key: 79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3

Scanning vault: /path/to/notes
Found 47 notes
Extracting links and generating events...
Processing  [####################################]  100%
Found 156 total links
Generated 47 events

Connecting to relay: ws://localhost:7777
Publishing events...
Publishing  [####################################]  100%

==================================================
Successfully imported: 47/47 notes

Your notes are now on Nostr!
Public key: 79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3

Get a Note

# Query by d-tag (normalized filename)
zettlkastr get zettelkasten-method

# With explicit key/relay
zettlkastr get zettelkasten-method --key <privkey> --relay ws://localhost:7777

Output:

============================================================
  Zettelkasten Method
============================================================

The Zettelkasten method is a personal knowledge management system.

# Zettelkasten Method

The Zettelkasten method is a personal knowledge management system.
It emphasizes [[Atomic Notes]] and [[Linking Your Thinking]].

Key principles:
- One idea per note
- Link liberally
- Use unique identifiers

────────────────────────────────────────────────────────────
Links to (4):
  • atomic-notes
  • linking-your-thinking
  • note-taking
  • knowledge-management

Query Backlinks

# Show all notes that link to a specific note
zettlkastr backlinks zettelkasten-method

Output:

Finding backlinks to: zettelkasten-method

============================================================
  Backlinks to 'zettelkasten-method' (3)
============================================================

1. Personal Knowledge Management (pkm)
2. Note-Taking Systems (note-taking-systems)
3. How I Organize My Thoughts (my-organization)

View Full Link Graph

# Display all notes with their connections
zettlkastr graph

# Export as JSON
zettlkastr graph --format json

Web UI

Launch the interactive web interface:

zettlkastr web

This starts a web server at http://localhost:8000 with:

Features:

  • Notes View: Browse and read notes with clickable wikilinks
  • List View: See all notes with forward links and backlinks
  • Network View: Interactive force-directed graph (Obsidian-style)
    • Drag nodes to rearrange
    • Click nodes to navigate
    • Double-click to focus
    • Toggle physics simulation
    • Zoom and pan

Rendered Markdown:

  • Full GitHub Flavored Markdown support
  • Styled headings, lists, code blocks, tables
  • Wikilinks converted to clickable links: [[Note]] → clickable
  • Broken links shown in red

Interactive Graph:

  • Nodes sized by number of connections
  • Arrows show link direction
  • Dark theme for better visibility
  • Controls: toggle physics, fit to screen, reset zoom

Options:

zettlkastr web --port 8080              # Custom port
zettlkastr web --host 0.0.0.0           # Bind to all interfaces
zettlkastr web --reload                 # Enable auto-reload (dev mode)

The web UI requires FastAPI and uvicorn:

pip install fastapi uvicorn[standard]

Run as Module

# Alternative way to run
python -m src import-vault /path/to/notes
python -m src get zettelkasten-method
python -m src info

How It Works

1. File Parsing

  • Recursively scans directory for .md files
  • Skips .obsidian, .trash, .git, and hidden directories
  • Parses YAML frontmatter if present
  • Extracts file modification time for event timestamps

2. Link Extraction

Supports Obsidian wikilink syntax:

  • Basic: [[Note Name]]
  • Display text: [[Note|Custom Display]]
  • Headings: [[Note#Section]]
  • Blocks: [[Note#^block-id]]

Ignores embeds: ![[Image.png]]

3. d-tag Normalization

Filenames are normalized per NIP-54:

Filename d-tag
Zettelkasten Method.md zettelkasten-method
2024-01-01.md 2024-01-01
C++ Programming.md c-programming
Note (Draft).md note-draft

4. Event Generation

Each note becomes a NIP-54 event:

{
  "id": "<sha256-hash>",
  "pubkey": "<your-pubkey>",
  "created_at": 1733097600,
  "kind": 30818,
  "tags": [
    ["d", "zettelkasten-method"],
    ["title", "Zettelkasten Method"],
    ["a", "30818:<pubkey>:atomic-notes", "ws://localhost:7777"],
    ["a", "30818:<pubkey>:linking-your-thinking", "ws://localhost:7777"]
  ],
  "content": "<full-markdown-content>",
  "sig": "<schnorr-signature>"
}

5. Backlinks

To find notes linking to "zettelkasten-method":

{
  "kinds": [30818],
  "#a": ["30818:<pubkey>:zettelkasten-method"]
}

The relay returns all events containing that a tag.

Project Structure

zettlkastr/
├── README.md
├── SPECIFICATION.md
├── requirements.txt
├── setup.py
├── .env.example
├── .gitignore
├── src/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cli.py          # CLI interface
│   ├── parser.py       # Vault parsing and link extraction
│   ├── events.py       # Event generation and signing
│   ├── relay.py        # Relay client (WebSocket)
│   └── utils.py        # Utilities (normalize_d_tag)
├── tests/
│   ├── fixtures/
│   │   └── sample_vault/
│   │       ├── Note A.md
│   │       ├── Note B.md
│   │       └── Note C.md
│   └── (test files)
└── scripts/
    └── (helper scripts)

Testing

Try the sample vault:

# Start strfry in one terminal
cd /path/to/strfry
./strfry relay

# In another terminal, import the sample vault
cd /path/to/zettlkastr
zettlkastr import-vault tests/fixtures/sample_vault

# Query a note
zettlkastr get note-a

Troubleshooting

Cannot connect to relay

Error: Failed to connect to relay at ws://localhost:7777

Solution: Start strfry:

cd /path/to/strfry
./strfry relay

Invalid private key

Error: Private key must be 64 hex characters (got 66)

Solution: Ensure you're using the private key (not public key) from nak key generate. It should be exactly 64 hexadecimal characters.

No markdown files found

No markdown files found in vault.

Solution: Check that:

  • Path is correct: ls /path/to/vault
  • Directory contains .md files
  • Files aren't in excluded directories (.obsidian, .trash)

Events not appearing

If events publish successfully but don't appear in queries:

  1. Verify event was accepted: look for "Successfully imported" message
  2. Check d-tag normalization: zettlkastr get <d-tag>
  3. Query strfry directly:
    echo '["REQ", "test", {"kinds": [30818], "limit": 10}]' | websocat ws://localhost:7777

Development

Install dev dependencies

pip install -r requirements.txt
pip install -e ".[dev]"

Run tests

pytest
pytest --cov=src  # With coverage

Code structure

  • parser.py: File scanning, frontmatter, wikilink regex
  • events.py: NIP-01 event structure, SHA-256 ID, Schnorr signatures
  • relay.py: WebSocket protocol, EVENT/OK/REQ/EOSE messages
  • cli.py: Click commands, progress bars, error handling

Future Enhancements

See SPECIFICATION.md for the full roadmap:

  • ctrl-p like fuzzy search by note names
  • Multi-relay support: Publish to multiple public relays
  • wss:// support: Connect to secure WebSocket relays
  • Incremental updates: Only publish changed notes
  • Sync: Bidirectional sync with Obsidian
  • nostrdb: Embedded database for offline-first operation
  • Encryption: NIP-44 for private notes
  • Search: Full-text search across all notes
  • Tags: Support for Obsidian tags and filtering

Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Submit a pull request

License

MIT License - see LICENSE file for details

Resources

Contact

  • GitHub Issues: Report bugs and request features
  • Nostr: Find the maintainers on Nostr!

Built with love for the zettelkasten and Nostr communities.

About

Obsidian <-> Nostr wiki

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published