markbase 0.3.0

A high-performance CLI tool for indexing and querying Markdown files for AI agent. Obsidian-compatible.
Documentation

Markdown Base CLI (markbase)

A high-performance CLI tool for indexing and querying Markdown notes, designed for both AI agents and human users with Obsidian compatibility in mind.

Ask DeepWiki

Installation

From crates.io (recommended):

cargo install markbase

Build from source:

git clone <repository-url>
cd markbase
cargo build --release
./target/release/markbase --help

Prerequisites: Rust 1.85+ (DuckDB is bundled)

Quick Start

export MARKBASE_BASE_DIR=/path/to/your/notes

markbase index
markbase query "author == 'Tom'"
markbase query "SELECT file.path, file.name FROM notes WHERE list_contains(file.tags, 'todo')"

Environment Variables

Variable Description Default
MARKBASE_BASE_DIR Vault directory . (current directory)
MARKBASE_OUTPUT_FORMAT Output format for query/template list table

Priority: CLI args > Environment variables > Defaults

export MARKBASE_BASE_DIR=/path/to/notes
export MARKBASE_OUTPUT_FORMAT=json

markbase index
markbase query "list_contains(file.tags, 'design')"

Concepts

Note Properties

Each indexed note has two namespaces for properties:

File Properties (file.* prefix): Access native database columns representing file metadata:

Field Type Description
file.path TEXT File path relative to base-dir
file.folder TEXT Directory path relative to base-dir
file.name TEXT File name without extension
file.ext TEXT File extension
file.size INTEGER File size in bytes
file.ctime TIMESTAMPTZ Created time
file.mtime TIMESTAMPTZ Modified time
file.tags VARCHAR[] Tags from content (#tag) and frontmatter
file.links VARCHAR[] Wiki-links [[link]] + embeds ![[embed]] from body and frontmatter
file.backlinks VARCHAR[] Notes linking to this note (reverse of links)
file.embeds VARCHAR[] Embeds ![[embed]] from body only

Note Properties (note.* prefix or bare): Access YAML frontmatter fields:

---
title: My Note
author: John
status: in-progress
---

Query using explicit prefix or bare shorthand:

markbase query "note.author == 'John'"    # explicit
markbase query "author == 'John'"         # shorthand (same result)

Tags

Tags are extracted from two sources:

Content tags (#tag in note body):

  • Obsidian format: # followed by alphanumeric characters, underscores, hyphens, and forward slashes
  • Must contain at least one non-numerical character (e.g., #1984 is invalid, #y1984 is valid)
  • Case-insensitive (e.g., #tag and #TAG are identical)
  • Supports nested tags using / separator (e.g., #project/2024/q1)

Frontmatter tags:

  • YAML list format: tags: [tag1, tag2] or tags: [project/2024]

All tags are merged into file.tags and can be queried with list_contains(file.tags, 'tag-name').

Field Resolution

Syntax Resolves To Example
file.* Native database column file.namename column
note.* Frontmatter JSON extraction note.authorproperties->"author"
bare (no prefix) Frontmatter JSON extraction (shorthand for note.*) authorproperties->"author"

The file.* and note.* namespaces are completely separate — no naming conflicts.

Name Uniqueness

Note names must be unique across the entire vault, regardless of their directory location.

  • Index: When indexing, if two notes have the same name (different paths), a warning is shown and the duplicate is skipped
  • Create: Creating a note fails if a note with that name already exists
  • Rename: Renaming a note fails if a note with the target name already exists

Link Format (Obsidian Style)

Always use the filename only — no path, no extension:

# ✅ Correct
[[中国移动]]
[[张三]]

# ❌ Wrong
[[entities/中国移动.md]]
[[people/张三]]

Wiki-links in frontmatter properties must additionally be wrapped in quotes:

# ✅ Correct
related_customer: "[[中石油]]"
attendees_internal: ["[[张三]]", "[[李四]]"]

# ❌ Wrong
related_customer: [[中国移动]]
attendees_internal: [[[张三]], [[李四]]]

Commands

index

Index Markdown notes to DuckDB.

markbase index              # Index base directory
markbase index --force      # Rebuild from scratch
markbase index -v           # Verbose output

Features:

  • Incremental updates (skips unchanged files)
  • Detects and removes deleted files
  • Obsidian-compatible (wiki-links, embeds, frontmatter, tags)

query

Query indexed notes.

Two input modes:

# Expression mode (WHERE clause only)
markbase query "note.author == 'Tom'"          # frontmatter (explicit)
markbase query "author == 'Tom'"               # frontmatter (shorthand)
markbase query "file.mtime > '2024-01-01'"     # file metadata
markbase query "list_contains(file.tags, 'project')"  # file array field
markbase query "author == 'Tom' ORDER BY file.mtime DESC LIMIT 10"

# SQL mode (full SELECT statement)
markbase query "SELECT file.path, note.author FROM notes WHERE note.author = 'Tom'"

Output formats:

markbase query "list_contains(tags, 'todo')" -o json
markbase query "list_contains(tags, 'todo')" -o list

Debug:

markbase query --dry-run "author == 'Tom'"  # Show translated SQL

Type casts for non-string comparisons:

markbase query "note.year::INTEGER >= 2024"
markbase query "note.created::TIMESTAMP > '2024-01-01'"
# or using bare shorthand:
markbase query "year::INTEGER >= 2024"

note

Create and manage notes.

Create a note:

markbase note new my-note                    # Create in base-dir
markbase note new notes/my-note              # Create in subdirectory
markbase note new my-note --template daily   # Use template

Rename a note:

markbase note rename old-name new-name

Behavior:

  • Looks up note by name (not path)
  • Fails if name is ambiguous or new name exists
  • Updates all [[old-name]] links and ![[old-name]] embeds across the vault (body and frontmatter)
  • Preserves aliases, section anchors, and block IDs

Verify a note against its template schema:

markbase note verify <name>

Checks that the note conforms to all constraints defined in its referenced MTS template(s):

  • Directory location matches _schema.location
  • Required frontmatter fields are present
  • Field types and enum values are correct
  • Link fields point to notes of the expected type

Warnings are reported to stderr. Exit code is non-zero only on errors (e.g. missing note or template file).

Render a note (expand .base embeds):

markbase note render <n>            # list format (default)
markbase note render <n> -o table   # Markdown table
markbase note render <n> --dry-run  # show SQL without executing

Renders the note body to stdout. Each ![[*.base]] embed is replaced with query results from the corresponding Obsidian Base file. Non-.base embeds are passed through unchanged.

Supported filters: link(this), link("name"), file.hasLink(this.file), file.hasTag(), file.inFolder(), date comparisons, isEmpty(), contains().

Warnings (unsupported filters, missing base files) go to stderr. Exit code is non-zero only on hard errors (e.g. note not found).

template

Manage MTS templates.

markbase template list                  # List templates (table format)
markbase template list -o json          # JSON format
markbase template list -F "tags,type"   # Additional fields
markbase template describe daily        # Show template content

Templates are stored in templates/ under base-dir.

Query Syntax

markbase translates field names using explicit namespaces (file.* for file metadata, note.* or bare for frontmatter) to DuckDB queries. All DuckDB SQL keywords and operators are supported natively.

Commonly Used Functions:

  • list_contains(field, value) - Array containment
    • list_contains(file.tags, 'todo') - file array field (native)
    • list_contains(note.categories, 'work') - frontmatter array (cast to VARCHAR[])

Field Prefix Reference:

Prefix Namespace Use For Example
file. File properties Metadata columns file.name, file.mtime, file.size
note. Note properties Frontmatter fields note.author, note.status
(bare) Note properties Shorthand for note.* author, status

Examples:

# File metadata queries (require file.* prefix)
markbase query "file.folder == './notes'"
markbase query "file.mtime > '2024-01-01'"
markbase query "file.size > 10000"
markbase query "file.name LIKE '%meeting%'"
markbase query "list_contains(file.tags, 'todo')"

# Frontmatter queries (note.* prefix or bare)
markbase query "note.author == 'John'"
markbase query "author == 'John'"  # same as above
markbase query "note.status == 'active'"
markbase query "author IS NOT NULL"

# Combined queries
markbase query "author == 'John' AND file.mtime > '2024-01-01'"
markbase query "list_contains(file.tags, 'todo') AND status == 'active'"

License

MIT