Clearly
Markdown editor and knowledge base for Mac
Ask AI about Clearly
Powered by Claude Β· Grounded in docs
I know everything about Clearly. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
Clearly
Markdown editor and knowledge base for Mac.
Mac App Store Β· Direct Download Β· Website Β· @Shpigford
Write with syntax highlighting, link your thoughts with wiki-links, search everything, preview beautifully. Native macOS, no Electron, no subscriptions.
Features
Writing
- Syntax highlighting β headings, bold, italic, links, code blocks, tables, highlighted as you type
- Format shortcuts β βB bold, βI italic, βK links
- Extended markdown β ==highlights==, ^superscript^,
subscript, :emoji: shortcodes,[TOC]generation - Scratchpad β menubar scratch pad with a global hotkey
Knowledge
- Wiki-links β link documents with
[[wiki-links]], type[[to autocomplete - Backlinks β linked and unlinked mentions with one-click linking
- Tags β organize with #tags, browse in the sidebar
- Global search β full-text search across every document, ranked by relevance
- Document outline β navigable heading outline, click to jump
- File explorer β browse folders, bookmark locations, create and rename files
Preview
- GFM rendering β tables, task lists, footnotes, strikethrough
- KaTeX math β inline and block equations
- Mermaid diagrams β flowcharts, sequence diagrams from code blocks
- Code blocks β 27+ languages, line numbers, diff highlighting, one-click copy
- Callouts β NOTE, TIP, WARNING, and 15+ types, foldable
- Interactive β toggle checkboxes, zoom images, hover footnotes, double-click to jump to source
Integration
- AI / MCP server β built-in MCP server and
clearlyCLI expose your vault to AI agents. See clearly CLI and ClearlyMCP. - QuickLook β preview .md files in Finder with Space
- PDF export β export or print, page breaks handled
- Copy formats β markdown, HTML, or rich text
Screenshots
Prerequisites
- macOS 14 (Sonoma) or later
- Xcode 16+ with command-line tools (
xcode-select --install) - Homebrew (brew.sh)
- xcodegen β
brew install xcodegen
Dependencies (cmark-gfm, Sparkle, GRDB, MCP SDK) are pulled automatically by Xcode via Swift Package Manager.
Quick Start
git clone https://github.com/Shpigford/clearly.git
cd clearly
brew install xcodegen # skip if already installed
xcodegen generate # generates Clearly.xcodeproj from project.yml
open Clearly.xcodeproj # opens in Xcode
Then hit βR to build and run.
The Xcode project is generated from
project.yml. If you changeproject.yml, re-runxcodegen generate. Don't edit the.xcodeprojdirectly.
CLI build
xcodebuild -scheme Clearly -configuration Debug build
Project Structure
Clearly/
βββ ClearlyApp.swift # @main β DocumentGroup + menu commands (β1/β2)
βββ MarkdownDocument.swift # FileDocument conformance for .md files
βββ ContentView.swift # Mode picker, Editor β Preview switching
βββ EditorView.swift # NSViewRepresentable wrapping NSTextView
βββ MarkdownSyntaxHighlighter.swift # Regex-based highlighting via NSTextStorageDelegate
βββ PreviewView.swift # NSViewRepresentable wrapping WKWebView
βββ FileExplorerView.swift # Sidebar file browser with bookmarks and recents
βββ FileParser.swift # Parses frontmatter, wiki-links, tags from documents
βββ VaultIndex.swift # SQLite + FTS5 index for search, backlinks, tags
βββ CLIInstaller.swift # Installs ~/.local/bin/clearly symlink from Settings
βββ Theme.swift # Centralized colors (light/dark) and font constants
βββ Info.plist
ClearlyQuickLook/
βββ PreviewProvider.swift # QLPreviewProvider for Finder previews
βββ Info.plist
ClearlyCLI/ # `clearly` CLI binary + MCP server
βββ CLI/ # ArgumentParser subcommands + global options
βββ Core/ # Pure-function tool implementations
βββ MCP/ # MCP adapter (tool registry + dispatch)
ClearlyCLIIntegrationTests/ # XCTest suite driving MCP server in-process
βββ FixtureVault/ # Sample .md files exercising every tool
βββ *.swift # Per-tool + schema + error + path-guard tests
Shared/
βββ MarkdownRenderer.swift # cmark-gfm β HTML + post-processing pipeline
βββ PreviewCSS.swift # CSS for in-app preview and QuickLook
βββ MathSupport.swift # KaTeX injection
βββ MermaidSupport.swift # Mermaid injection
βββ SyntaxHighlightSupport.swift # Highlight.js injection
βββ EmojiShortcodes.swift # :shortcode: β Unicode lookup
βββ FrontmatterSupport.swift # Shared YAML frontmatter parser
βββ Resources/ # Bundled JS/CSS, demo.md
website/ # Static site deployed to clearly.md
scripts/ # Release pipeline + CLI smoke test
project.yml # xcodegen config (source of truth)
Architecture
SwiftUI + AppKit, document-based app with four targets.
Targets
- Clearly β main app.
DocumentGroupwithMarkdownDocument, editor and preview modes, file explorer, vault indexing. - ClearlyQuickLook β Finder extension for previewing
.mdfiles with Space. - ClearlyCLI β the
clearlyCLI binary and MCP server (same executable, different arg parser). Exposes 9 tools across read and write. See clearly CLI and ClearlyMCP. - ClearlyCLIIntegrationTests β XCTest suite driving the MCP server in-process via
InMemoryTransport. Runs on every PR via.github/workflows/test.yml.
Editor
Wraps AppKit's NSTextView via NSViewRepresentable β not SwiftUI's TextEditor. This provides native undo/redo, the system find panel (βF), and NSTextStorageDelegate-based syntax highlighting on every keystroke.
Preview
PreviewView wraps WKWebView and renders HTML via MarkdownRenderer (cmark-gfm). Post-processing pipeline: math β highlight marks β superscript/subscript β emoji β callouts β TOC β tables β code highlighting.
Knowledge Graph
VaultIndex maintains a SQLite database with FTS5 for full-text search. FileParser extracts wiki-links, backlinks, and tags from documents. The index is built on a background thread via WorkspaceManager to avoid blocking the UI.
Dependencies
| Package | Purpose |
|---|---|
| cmark-gfm | GitHub Flavored Markdown β HTML |
| Sparkle | Auto-updates (direct distribution only) |
| GRDB | SQLite + FTS5 for vault indexing |
| MCP | Model Context Protocol server |
| swift-argument-parser | CLI parsing for clearly |
Key Decisions
- AppKit bridge β
NSTextViewoverTextEditorfor undo, find, andNSTextStorageDelegatesyntax highlighting - Dynamic theming β all colors through
Theme.swiftwithNSColor(name:)for automatic light/dark - Shared code β
MarkdownRendererandPreviewCSScompile into both the main app and QuickLook - Dual distribution β Sparkle for direct, App Store without. All Sparkle code wrapped in
#if canImport(Sparkle) - No
.inspector()β outline panel usesHStackdue to fullscreen safe area bugs
Common Dev Tasks
Change syntax highlighting
Edit MarkdownSyntaxHighlighter.swift. Patterns are applied in order β code blocks first, then everything else.
Modify preview styling
Edit Shared/PreviewCSS.swift. Used by both in-app preview and QuickLook. Keep in sync with Theme.swift colors. Base styles must come before @media (prefers-color-scheme: dark) overrides.
Add a preview feature
Follow the MathSupport/MermaidSupport pattern: create a *Support.swift enum in Shared/ with a static method that returns a <script> block. Integrate into PreviewView.swift, PreviewProvider.swift, and PDFExporter.swift.
Testing
Automated:
xcodebuild test -scheme ClearlyCLIIntegrationTests -destination 'platform=macOS'
./scripts/cli-smoke.sh
CI runs both on every pull request (.github/workflows/test.yml).
Manual:
- Build and run (βR)
- Open a
.mdfile β verify syntax highlighting - Switch to preview (β2) β verify rendered output
- Test wiki-links, backlinks, search, tags
- QuickLook: select a
.mdin Finder, press Space - Check both light and dark mode
clearly CLI
The clearly command-line binary is bundled with Clearly.app and operates on the same SQLite index the app maintains β no separate configuration, no data duplication.
Install
Open Clearly β Settings β Command Line β Install. Creates a symlink at ~/.local/bin/clearly pointing to the bundled binary inside Clearly.app/Contents/Resources/Helpers/ClearlyCLI. No admin password needed β everything stays in your home folder.
If ~/.local/bin isn't already on your shell PATH, add this to ~/.zprofile (or your shell's equivalent) and open a new terminal:
export PATH="$HOME/.local/bin:$PATH"
Upgrades (Sparkle or App Store) keep the symlink valid. Uninstall from the same Settings pane.
Legacy /usr/local/bin/clearly installs from Clearly β€ 2.4.x keep working and are detected automatically β no action needed.
Subcommand reference
clearly
βββ mcp Start the MCP stdio server (this is what MCP clients invoke)
βββ search <query> Full-text search; emits NDJSON hits
βββ read <path> Read a note + metadata (hash, size, mtime, frontmatter, headings, tags)
βββ list List notes as NDJSON (fresh filesystem walk)
βββ headings <path> Heading outline (level, text, line_number)
βββ frontmatter <path> Parsed YAML frontmatter (flat key-value)
βββ backlinks <path> Linked references + unlinked mentions
βββ tags [<tag>] All tags with counts, or files for one tag
βββ create <path> Create a new note from --content or --from-stdin
βββ update <path> Update with --mode replace|append|prepend
βββ vaults [list] List loaded vaults (name, path, file_count, last_indexed_at)
βββ index [rebuild] Rebuild the SQLite index from disk
Run clearly <subcommand> --help for flags, examples, and output-shape notes.
Exit codes
| Code | Name | Meaning |
|---|---|---|
0 | success | Command completed; output on stdout |
1 | general | Generic failure (e.g. no vaults loaded, non-UTF8 file) |
2 | usage | Invalid arguments / missing required flags |
3 | notFound | Note or vault filter not found |
4 | permission | Path resolves outside vault (traversal, /absolute, unicode lookalikes) |
5 | conflict | Note already exists (on create) or ambiguous across vaults |
Output contract
- JSON mode (default): every tool emits a stable structured shape documented per-tool in its
--help. List-shaped commands (search,list,tags) emit NDJSON β one record per line β for stream-friendly piping. Keys are snake_case. - Text mode (
--format text): human-readable aligned output, no stability guarantees. Use for terminal eyeballing only; agents and scripts should stick with JSON. - Errors always go to stderr as a structured JSON object with
error(stable identifier),message(human text), and relevant context fields. See the error identifiers table.
Pipeline examples
# Top 20 tag counts, sorted
clearly tags | jq -s 'sort_by(-.count) | .[:20]'
# Grep every note under Projects/ for a term
clearly list --under Projects/ \
| jq -r '.relative_path' \
| xargs -I{} sh -c 'clearly read "{}" | jq -r ".content" | grep -l -e "OKR" /dev/stdin && echo "{}"'
# Cache invalidation by content hash
OLD=$(clearly read Notes/plan.md | jq -r '.content_hash')
# ...edit the file...
NEW=$(clearly read Notes/plan.md | jq -r '.content_hash')
[ "$OLD" != "$NEW" ] && echo "rebuild"
Troubleshooting
- Multi-vault ambiguity β pass
--in-vault <name>on per-command calls, or--vault <path>on the global command to scope which vault(s) to load. - Custom bundle id β
--bundle-id com.sabotage.clearly.devto point at the Debug build's vault store. - Dev-build SIGKILL β the bundled
ClearlyCLIinsideClearly Dev.app/Contents/Resources/Helpers/gets SIGKILL'd by macOS when launched standalone (code-signature invalidation). Use the product binary atBuild/Products/Debug/ClearlyCLIdirectly for local testing.
ClearlyMCP
Same binary, different arg β clearly mcp starts a stdio MCP server exposing 9 tools to any Model Context Protocol client.
Client config
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"clearly": {
"command": "/Users/you/.local/bin/clearly",
"args": ["mcp"]
}
}
}
Claude Code (~/.config/claude-code/mcp.json or via claude mcp add):
{
"mcpServers": {
"clearly": {
"command": "/Users/you/.local/bin/clearly",
"args": ["mcp"]
}
}
}
Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"clearly": {
"command": "/Users/you/.local/bin/clearly",
"args": ["mcp"]
}
}
}
Settings β Command Line β Copy MCP config in the Clearly app copies a ready-to-paste snippet with the correct path for your machine (flips to ~/.local/bin/clearly once the symlink is installed, or the legacy /usr/local/bin/clearly path if you're on an older install).
Tool reference
All tools use snake_case JSON keys on input and output. Every response also includes a structuredContent field on success and isError: true on failure β both mirror the text content exactly. Error responses include error (stable identifier) and message.
| Tool | Annotations | Summary |
|---|---|---|
search_notes | read-only, idempotent | Full-text search (BM25). Returns ranked hits with excerpts. |
read_note | read-only, idempotent | Full content + hash, size, mtime, frontmatter, headings, tags. Optional line range. |
list_notes | read-only, idempotent | Fresh filesystem walk. Optional under prefix. |
get_headings | read-only, idempotent | Heading outline (level 1β6, text, line_number). |
get_frontmatter | read-only, idempotent | Parsed YAML frontmatter as a flat map. |
get_backlinks | read-only, idempotent | Linked references (via [[WikiLink]]) plus unlinked mentions. |
get_tags | read-only, idempotent | All tags with counts, or files per tag. |
create_note | destructive, non-idempotent | New note at a vault-relative path. Conflict on existing. |
update_note | destructive, non-idempotent | replace / append / prepend modes. Prepend is frontmatter-aware. |
Each tool registers its full JSON Schema via MCP outputSchema; clients that render schemas (MCP Inspector, the Claude API tool-call viewer) can introspect every field without reading source.
Example payloads
search_notes (NDJSON, one hit per line):
{"excerpts":[{"context_line":"# The Death of SaaS Pricing Pages","line_number":8},{"context_line":"Pricing pages are brokenβ¦","line_number":10}],"filename":"The Death of SaaS Pricing Pages","matches_filename":true,"relative_path":"Blog Posts/The Death of SaaS Pricing Pages.md","vault":"Documents","vault_path":"/Users/β¦/Documents"}
read_note:
{
"content": "---\ntitle: Building in Public is a Lie\ndate: 2026-03-15\n---\n\n# Building in Public is a Lie\nβ¦",
"content_hash": "e9777e4a4e308a77ec7c5814f4d4204c978139249967deb064b4558bf4f2594a",
"frontmatter": { "date": "2026-03-15", "status": "draft", "tags": "writing, transparency", "title": "Building in Public is a Lie" },
"headings": [{ "level": 1, "line_number": 8, "text": "Building in Public is a Lie" }],
"size_bytes": 1703,
"modified_at": "2026-04-14T15:47:25.274Z",
"relative_path": "Blog Posts/Building in Public is a Lie.md",
"vault": "Documents"
}
get_backlinks:
{
"linked": [
{ "display_text": "my piece on transparency", "line_number": 23, "relative_path": "Blog Posts/The Death of SaaS Pricing Pages.md", "vault": "Documents" }
],
"unlinked": [],
"relative_path": "Blog Posts/Building in Public is a Lie.md",
"vault": "Documents"
}
get_tags (all tags, NDJSON):
{"count":1,"tag":"ai"}
{"count":33,"tag":"analysis"}
Error identifiers
Every error response β whether emitted by the CLI to stderr or by the MCP server as structuredContent with isError: true β uses one of these identifiers:
error | Where it fires | Context fields |
|---|---|---|
missing_argument | Required flag/arg not provided | argument |
invalid_argument | Bad value (e.g. --mode not one of replace/append/prepend) | argument, reason |
invalid_encoding | File is not valid UTF-8 | relative_path |
note_not_found | Target note doesn't exist in any loaded vault | relative_path |
path_outside_vault | Path resolves outside the vault (traversal, absolute, unicode lookalike) | relative_path |
ambiguous_path | Multiple loaded vaults contain this path | relative_path, matches |
note_exists | create_note against an existing path | relative_path |
no_vaults | CLI-only: could not open any vault index | bundle_id |
no_vault_match | --in-vault filter didn't match any loaded vault | filter |
unknown_tool | MCP tools/call for an unregistered tool name | tool |
internal_error | Uncategorized exception from a tool | error_type |
The identifier is the stable contract β agent code should branch on error, not on message text.
License
FSL-1.1-MIT β see LICENSE. Code converts to MIT after two years.
