Semantic Search for Your Obsidian Vault

Build a CLI tool that ingests Obsidian notes into a semantic memory engine.

Introduction

30 Second Summary

You have notes scattered across dozens of files. The idea you wrote three months ago connects perfectly to a project you started last week, but you will never find that connection because search only works when you remember the exact words you used.

In this project, you will build a Python CLI tool called VaultMind that turns your Obsidian vault into a semantically searchable knowledge base using Supermemory Local's AI memory engine.

What You'll Build

Ask your notes a question in plain English and get back ranked results powered by semantic search, surfacing connections you forgot existed.

By the end of this project, you'll have:

  • An ingest pipeline that loads any folder of notes into Supermemory Local, making your entire vault searchable by meaning instead of keywords.
  • A discover command that finds notes semantically related to a given note but not already wiki-linked, revealing hidden connections in your knowledge base.
  • Rich-formatted terminal output with similarity scores, progress bars, and styled panels that demo beautifully in a terminal recording.
  • Secret Mission: A conversational "ask" command that uses Gemini to synthesize direct answers from your notes with source attribution.

Are there any prerequisites?

You'll need Python 3.10 or higher installed, a Google account for a free Gemini API key, and basic familiarity with using a terminal.

Before We Start

Before diving into code, let's lock in what you're about to build and why it matters. VaultMind solves a real gap: Obsidian's built-in search only matches exact keywords, so two notes about the same concept written in different words will never find each other.

Install Supermemory Local and Dependencies

VaultMind needs two backends to function: a memory engine that stores and retrieves your notes by meaning, and an LLM that extracts semantic understanding from raw markdown.

In this step, you'll grab a free Gemini API key for the LLM processing, install Supermemory Local as the memory backend, and set up your Python project with all its dependencies.

In this step, get ready to:
  • Obtain a Gemini API key from Google AI Studio.
  • Install and configure Supermemory Local with Gemini as the LLM provider.
  • Create the project directory with a virtual environment and install dependencies.
Get a Gemini API key from Google AI Studio

The Supermemory setup wizard will ask for an LLM provider key in a moment. You need to have your Gemini API key ready before starting the server.

  • Open your browser and go to https://aistudio.google.com.
  • Sign in with your Google account.
  • New users get a default project and API key created automatically. Navigate to the API keys page in the left sidebar.
  • Copy your API key and save it somewhere safe (a text file or notes app). You will paste it in the next substep.

Why Gemini?

Gemini's free tier provides free input and output tokens with no hardware requirements on your machine. Supermemory Local uses this LLM to extract semantic meaning from your notes during ingestion.

Use gemini-2.5-flash or newer. Older models like gemini-2.0-flash have been reported to have 0 free-tier rate limits.

Not seeing an API key?

If no key appears automatically, click Create API key on the API keys page. Select the default project when prompted.

help me get a Gemini API key from Google AI Studio

Install Supermemory Local and start the server

Supermemory Local is a single binary that runs a complete memory engine on your machine. No Docker, no database, no config files required.

  • Install the Supermemory Local binary by running this command:
curl -fsSL https://supermemory.ai/install | bash

What does this command do?

The installer detects your OS and architecture, downloads the correct binary, and verifies it. It supports Linux (x64 and arm64).

You should see output confirming the binary was downloaded and installed successfully.

Seeing a permission error?

If the installer fails with a permission error, try running it with sudo or check that your user has write access to /usr/local/bin.

help me fix a permission error when installing Supermemory Local

  • Start the Supermemory server by running:
supermemory-server

What happens on first boot?

Since no LLM provider key is set yet, the server launches an interactive setup wizard. It will ask you to pick a provider and paste your key. The server runs in the foreground and takes over this terminal window.

  • When the wizard asks for a provider, select Gemini.
  • Paste your Gemini API key when prompted. The key is saved encrypted for future launches.
  • When asked about an embedding model, press Enter to keep the default local embeddings (Xenova/bge-base-en-v1.5).

After configuration, the server displays a status panel like this:

  ┌──────────────────────────────────────────────────┐
  │  url       http://localhost:6767                 │
  │  database  ./.supermemory                        │
  │  api key   sm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx     │
  │  org id    xxxxxxxxxxxxxxxxxxxxxx                │
  └──────────────────────────────────────────────────┘
  • Copy the sm_... API key from this panel and save it. You will need it shortly.
  • Confirm you see http://localhost:6767 in the output. The server is now running.

Keep this terminal open

The server runs in the foreground. Open a new terminal window for the remaining commands in this step. If you close this terminal, the server stops.

Server fails to start?

Make sure port 6767 is not already in use. You can check with lsof -i :6767. If another process is using it, stop that process first.

help me troubleshoot Supermemory Local not starting

Set up the Python project and install dependencies

With the server running in one terminal, open a new terminal window for your project work.

  • Open a new terminal window. Press Ctrl+Alt+T (Linux) to launch one.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and move into it by running these commands:
mkdir vaultmind
cd vaultmind
  • Confirm you are in the vaultmind folder by running:
pwd

You should see a path ending in /Desktop/vaultmind.

  • Create a Python virtual environment and activate it by running:
python3 -m venv .venv
source .venv/bin/activate

Your terminal prompt now shows (.venv) at the beginning. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python, so different projects never conflict with each other.

  • Install the project dependencies by running:
pip install supermemory==3.49.0 click==8.4.2 rich==15.0.0

You should see pip downloading and installing the three packages, ending with a "Successfully installed" message.

What are these packages?

  • supermemory is the Python SDK for talking to the Supermemory Local server.
  • click is a framework for building command-line interfaces in Python.
  • rich adds colors, tables, progress bars, and panels to terminal output.

Pip install failed?

Make sure your virtual environment is active (you see (.venv) in your prompt). If not, run source .venv/bin/activate again.

help me fix a pip install failure in my virtual environment

  • Create the main file and export your Supermemory API key by running:
touch vaultmind.py
export SUPERMEMORY_API_KEY="your-supermemory-api-key-here"
  • Replace your-supermemory-api-key-here with the sm_... key you saved from the server startup panel.

Before you verify, what do you think the following Python import will do if everything is set up correctly?

  • Verify everything works by running this command:
python3 -c "from supermemory import Supermemory; print('SDK ready')"

You should see SDK ready printed to the terminal. This confirms the Supermemory Python SDK is installed and importable.

  • Also confirm vaultmind.py exists by running:
ls

You should see vaultmind.py and the .venv folder in the output.

Seeing ModuleNotFoundError?

If you see an import error, check that your virtual environment is active ((.venv) in the prompt) and that the pip install command completed successfully. Re-run the install if needed.

help me fix a ModuleNotFoundError for supermemory

Your memory backend is running and your Python project is ready. Next up, you'll build the vault parser that reads your Obsidian notes and feeds them into Supermemory.

Ingest Your Vault

Your Supermemory Local server is running and your project skeleton is ready. The tool is useless without data.

In this step you will feed an Obsidian vault into Supermemory Local so the memory engine can extract semantic meaning and create embeddings from every note. By the end, a single CLI command loads your entire vault into Supermemory's memory layer.

In this step, get ready to:
  • Create a sample Obsidian vault with markdown notes covering distinct topics.
  • Build a parser that extracts frontmatter, body text, and wiki-links from each note.
  • Write an ingest CLI command that sends every note to Supermemory Local.
Create a sample Obsidian vault for testing

You need test data before you can verify the ingest pipeline. You will create a small vault of 8 markdown notes that mimic a real Obsidian vault with YAML frontmatter, body content, and [[wiki-links]] between notes.

  • Create the sample vault directory by running this command:
mkdir sample-vault
  • Create sample-vault/programming-ideas.md with the following content:
---
tags: [programming, ideas, projects]
---

# Programming Ideas

Some project ideas I want to explore:

- Build a CLI tool that connects to a local AI memory system
- Create a personal knowledge graph from my notes
- Write a Rust-based file watcher that triggers actions

I've been thinking about how [[distributed-systems]] concepts could apply to personal tools.

Also related to my thoughts in [[learning-rust]].

What is this file structure?

Obsidian notes are plain .md files. The --- block at the top is YAML frontmatter where you store tags and metadata.

The [[distributed-systems]] syntax is an Obsidian wiki-link that connects this note to another. Your discover command will use these links later to filter out already-connected notes.

  • Create sample-vault/distributed-systems.md with the following content:
---
tags: [programming, architecture]
---

# Distributed Systems

Notes on distributed systems concepts:

- CAP theorem: you can only have two of Consistency, Availability, Partition tolerance
- Event sourcing keeps a log of all state changes
- CRDTs allow conflict-free replication

These ideas apply broadly, even to personal productivity tools that sync across devices.
  • Confirm the first two files exist by running:
ls sample-vault

You should see distributed-systems.md and programming-ideas.md listed.

  • Create sample-vault/learning-rust.md with the following content:
---
tags: [programming, rust, learning]
---

# Learning Rust

My journey learning Rust:

- Ownership and borrowing are the key concepts to master
- The compiler is strict but helpful
- Great for CLI tools and systems programming

I want to rebuild some of my Python scripts in Rust for performance. See [[programming-ideas]] for specific projects.
  • Create sample-vault/cooking-sourdough.md with the following content:
---
tags: [cooking, baking, fermentation]
---

# Sourdough Bread

My sourdough notes:

- Feed starter every 12 hours with equal parts flour and water
- Bulk ferment 4-6 hours at room temperature
- The key is patience and consistent timing

Fermentation is fascinating. The biological processes are similar to how distributed systems achieve eventual consistency. Both rely on time and environment.
  • Confirm you now have 4 files by running:
ls sample-vault

You should see 4 markdown files listed.

  • Create sample-vault/book-notes-atomic-habits.md with the following content:
---
tags: [books, productivity, habits]
---

# Atomic Habits - Book Notes

Key takeaways from Atomic Habits by James Clear:

- Habits are the compound interest of self-improvement
- Make it obvious, attractive, easy, and satisfying
- Identity-based habits are more durable than outcome-based ones
- Environment design matters more than motivation

This connects to how I think about building systems and automating repetitive tasks.
  • Create sample-vault/personal-knowledge-management.md with the following content:
---
tags: [productivity, pkm, notes]
---

# Personal Knowledge Management

How I think about organizing knowledge:

- Links between ideas are more valuable than folders
- Regular review surfaces forgotten connections
- The goal is retrieval, not storage
- Tools should get out of the way

The challenge is that keyword search misses conceptual connections. Two notes about the same topic might use completely different vocabulary.
  • Confirm you now have 6 files by running:
ls sample-vault
  • Create sample-vault/machine-learning-basics.md with the following content:
---
tags: [programming, ml, ai]
---

# Machine Learning Basics

Core concepts:

- Embeddings turn text into vectors that capture meaning
- Similar concepts have similar vector representations
- This enables semantic search: finding things by meaning, not keywords
- Neural networks learn these representations from data

Vector similarity is powerful for knowledge management. It could find connections between notes that share no common words.
  • Create sample-vault/weekly-review-june.md with the following content:
---
tags: [journal, review]
---

# Weekly Review - June

What went well:
- Made progress on the Rust learning path
- Baked two successful sourdough loaves
- Read 3 chapters of Atomic Habits

What to focus on next:
- Start building that CLI tool project I keep thinking about
- Connect my programming knowledge with my productivity system
- Explore how AI could help with note-taking workflows
  • Verify all 8 sample notes are in place by running:
ls sample-vault | wc -l

You should see 8 printed. Your test vault is ready.

Seeing fewer than 8 files?

Double-check you created each file inside the sample-vault/ directory, not in the project root. Run ls sample-vault to see the full listing and compare filenames against the list above.

help me figure out why my sample vault files are missing

Build the vault parser and ingest command

Your vault parser needs to handle three things for each note: YAML frontmatter (tags), the markdown body, and wiki-links. You also need PyYAML to parse the frontmatter.

  • Install PyYAML by running:
pip install pyyaml==6.0.2
  • Verify the install succeeded by running:
python3 -c "import yaml; print('PyYAML ready')"

You should see PyYAML ready printed.

  • Open vaultmind.py in your editor and add the imports, constants, and client helper:
import os
import re
import time
import yaml
import click
from pathlib import Path
from supermemory import Supermemory
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import track

console = Console()

SUPERMEMORY_API_KEY = os.environ.get("SUPERMEMORY_API_KEY", "")
SUPERMEMORY_BASE_URL = "http://localhost:6767"
CONTAINER_TAG = "vault"


def get_client():
    if not SUPERMEMORY_API_KEY:
        console.print("[red]Error: SUPERMEMORY_API_KEY not set.[/red]")
        console.print("Export it: export SUPERMEMORY_API_KEY='sm_...'")
        raise SystemExit(1)
    return Supermemory(api_key=SUPERMEMORY_API_KEY, base_url=SUPERMEMORY_BASE_URL)

What does this code do?

  • SUPERMEMORY_API_KEY reads the key you exported in Step 1 from the environment.
  • CONTAINER_TAG groups all vault memories under one label so you can search within just this vault later.
  • get_client() creates a Supermemory SDK client pointing at your local server and exits early with a helpful message if the key is missing.
  • Below get_client(), add the parse_note() function that extracts frontmatter, body, and wiki-links:
def parse_note(filepath):
    text = filepath.read_text(encoding="utf-8")
    frontmatter = {}
    body = text

    if text.startswith("---"):
        parts = text.split("---", 2)
        if len(parts) >= 3:
            try:
                frontmatter = yaml.safe_load(parts[1]) or {}
            except yaml.YAMLError:
                frontmatter = {}
            body = parts[2].strip()

    wiki_links = re.findall(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]", body)

    return {
        "frontmatter": frontmatter,
        "body": body,
        "wiki_links": wiki_links,
        "tags": frontmatter.get("tags", []) or [],
    }

What does this code do?

  • Splits the file at the --- markers to isolate YAML frontmatter from the markdown body.
  • Uses a regex to find all [[wiki-link]] patterns, handling the optional [[note|display text]] format.
  • Returns a dictionary with the parsed frontmatter, body text, extracted wiki-links, and tags. The discover command will use wiki_links to filter out already-connected notes.
  • Save vaultmind.py and verify the parser works by running:
python3 -c "from vaultmind import parse_note; from pathlib import Path; r = parse_note(Path('sample-vault/programming-ideas.md')); print(f'Tags: {r[\"tags\"]}'); print(f'Links: {r[\"wiki_links\"]}')"

You should see Tags: ['programming', 'ideas', 'projects'] and Links: ['distributed-systems', 'learning-rust'] printed.

Seeing an import error?

Make sure your virtual environment is still activated. You should see (.venv) in your terminal prompt. If not, run source .venv/bin/activate again.

help me fix an import error when running my vaultmind parser

  • Add the get_md_files() helper below parse_note():
def get_md_files(vault_path):
    vault = Path(vault_path)
    if not vault.is_dir():
        console.print(f"[red]Error: {vault_path} is not a directory.[/red]")
        raise SystemExit(1)
    return sorted(vault.rglob("*.md"))

What does this code do?

Recursively finds all .md files in the vault directory using rglob. Sorting ensures consistent ordering across runs. Exits early with a clear error if the path is not a valid directory.

Add the Supermemory client connection and ingest loop

With parsing in place, you can now wire up the CLI command that loops through every note and sends it to Supermemory Local via client.add().

  • Below get_md_files(), add the Click CLI group and the ingest command header:
@click.group()
def cli():
    """VaultMind - Semantic search for your Obsidian vault."""
    pass


@cli.command()
@click.argument("vault_path", type=click.Path(exists=True))
def ingest(vault_path):
    """Ingest an Obsidian vault into Supermemory Local."""
    client = get_client()
    md_files = get_md_files(vault_path)

    if not md_files:
        console.print("[yellow]No .md files found in vault.[/yellow]")
        return

    console.print(f"[bold]Found {len(md_files)} notes to ingest.[/bold]\n")
    ingested = 0

What does this code do?

  • @click.group() creates a CLI app called cli that can hold multiple subcommands (ingest, search, discover).
  • @cli.command() registers ingest as a subcommand. Click validates the vault path exists before the function runs.
  • The function connects to Supermemory, collects all markdown files, and prepares the ingestion counter.
  • Directly below the ingested = 0 line (still inside the ingest function), add the ingestion loop:
    for filepath in track(md_files, description="Ingesting notes..."):
        note = parse_note(filepath)
        relative_path = str(filepath.relative_to(vault_path))

        content = f"# {relative_path}\n\n{note['body']}"

        metadata = {}
        if note["tags"]:
            metadata["tags"] = ",".join(note["tags"])
        if note["wiki_links"]:
            metadata["wiki_links"] = ",".join(note["wiki_links"])

        try:
            client.add(
                content=content,
                container_tag=CONTAINER_TAG,
                custom_id=relative_path,
                metadata=metadata,
            )
            ingested += 1
        except Exception as e:
            console.print(f"[red]Failed to ingest {relative_path}: {e}[/red]")

        time.sleep(1)

    console.print(
        f"\n[green bold]Ingested {ingested} notes into Supermemory.[/green bold]"
    )

What does this code do?

  • track() from Rich wraps the file list in a live progress bar so you see ingestion happening in real time.
  • client.add() sends each note's content and metadata to Supermemory Local. The custom_id is the relative file path so you can trace results back to specific notes.
  • time.sleep(1) adds a 1-second delay between requests per Supermemory's recommended rate limit for batch ingestion.
  • Add the entry point at the very bottom of vaultmind.py:
if __name__ == "__main__":
    cli()
  • Save vaultmind.py.
  • Create a requirements.txt file in your vaultmind directory with the following content:
supermemory==3.49.0
click==8.4.2
rich==15.0.0
pyyaml==6.0.2

Before you run the ingest, what do you think will happen? Will all 8 notes be processed, or will something fail?

  • Run the ingest command:
python3 vaultmind.py ingest ./sample-vault

You should see a Rich progress bar advancing through 8 notes, followed by the message:

Ingested 8 notes into Supermemory.

Seeing a connection error?

Make sure your Supermemory Local server is still running in another terminal. You should see http://localhost:6767 in its startup output. If it stopped, restart it with supermemory-server.

Also confirm your SUPERMEMORY_API_KEY is still exported in this terminal session. Run echo $SUPERMEMORY_API_KEY to check.

help me fix a connection error when running the ingest command

✔️ Awesome, I've got everything!

Great. Double check you have saved vaultmind.py and requirements.txt.

ⓧ I'd like to double check the full code

Your vaultmind.py should contain these sections in order. Scroll up and confirm each one is present:

  • Imports (os, re, time, yaml, click, Path, Supermemory, Rich imports).
  • Constants (SUPERMEMORY_API_KEY, SUPERMEMORY_BASE_URL, CONTAINER_TAG).
  • get_client() function.
  • parse_note() function.
  • get_md_files() function.
  • @click.group() cli() and @cli.command() ingest() with the full ingestion loop.
  • if __name__ == "__main__": cli() at the bottom.

Your vault is now loaded into Supermemory's memory engine. Next up, you will query it with natural language and see semantic search find notes that keyword search would miss entirely.

Search Your Notes Semantically

Your vault is loaded into Supermemory Local. Eight notes are sitting in the memory engine, processed into embeddings that capture their meaning. Now it is time to prove that semantic search actually works better than keywords.

In this step, you will build a search command that takes a natural-language question and returns ranked results from your vault. You will see it find notes that match the meaning of your question, even when they do not contain the exact words you typed.

In this step, get ready to:
  • Build a search command that queries Supermemory with natural language and displays results in a Rich table.
  • Test semantic queries that prove VaultMind finds notes by meaning, not exact words.
Build the search command

The search command connects a natural-language query to Supermemory's hybrid search mode. Hybrid search combines extracted memories with raw document chunks, giving you the best of both retrieval strategies.

  • In vaultmind.py, find the if __name__ == "__main__": line at the bottom of the file.
  • Add the following search command above that line:
@cli.command()
@click.argument("query")
@click.option("--limit", default=5, help="Number of results to return.")
@click.option("--threshold", default=0.5, help="Minimum similarity score (0-1).")
def search(query, limit, threshold):
    """Search your vault with natural language."""
    client = get_client()

    console.print(f'[bold]Searching for:[/bold] "{query}"\n')

    # Query Supermemory using hybrid search (memories + document chunks)
    results = client.search.memories(
        q=query,
        container_tag=CONTAINER_TAG,
        search_mode="hybrid",
        limit=limit,
        threshold=threshold,
    )

    # Handle empty results gracefully
    if not results.results:
        console.print("[yellow]No results found. Try a different query or lower the threshold.[/yellow]")
        return

What does this code do?

This defines the CLI skeleton for the search command with two options the learner can tweak.

  • --limit controls how many results come back (default 5).
  • --threshold sets a minimum similarity score between 0 and 1. Results below this score are filtered out.
  • search_mode="hybrid" tells Supermemory to search both extracted memories and raw document chunks for the best coverage.
  • Directly below the code you just pasted (still inside the search function), add the Rich table formatting:
    # Build a Rich table to display results
    table = Table(title="Search Results", show_lines=True)
    table.add_column("#", style="dim", width=3)
    table.add_column("Score", style="cyan", width=6)
    table.add_column("Content", style="white")

    # Populate rows with rank, similarity score, and content snippet
    for i, result in enumerate(results.results, 1):
        content = result.memory or result.chunk or ""
        snippet = content[:200] + "..." if len(content) > 200 else content
        score = f"{result.similarity:.2f}"
        table.add_row(str(i), score, snippet)

    console.print(table)
    console.print(f"\n[dim]Found {len(results.results)} results in {results.timing}ms[/dim]")

What does this code do?

This takes the raw search results and turns them into a readable Rich table with three columns.

  • Each result has a memory (extracted fact) or a chunk (raw document piece). The code grabs whichever exists.
  • Content is truncated to 200 characters so the table stays readable in the terminal.
  • The timing field in the response tells you how many milliseconds the search took.
  • Save vaultmind.py.
  • Test the search command by running:
python3 vaultmind.py search "what are my project ideas"

You should see a formatted table titled "Search Results" with numbered rows, similarity scores, and content snippets from your ingested notes.

Not seeing any results?

  • Make sure Supermemory Local is still running in your other terminal. If you closed it, restart it with supermemory-server.
  • Check that your SUPERMEMORY_API_KEY environment variable is still set. If you opened a new terminal, you need to export it again.
  • Confirm you ran the ingest command in the previous step. Supermemory needs data before it can search.

Help me debug why my VaultMind search returns no results.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Your vaultmind.py is now long, so confirm these sections are present in order:

  • Imports and constants at the top (os, re, time, yaml, click, Path, Supermemory, Rich imports).
  • get_client() function.
  • parse_note() function.
  • get_md_files() function.
  • The @click.group() cli function.
  • The ingest command (from the previous step).
  • The new search command you just added.
  • if __name__ == "__main__": cli() at the very bottom.
Test with semantic queries that prove the value

The real power of semantic search is finding notes that match your intent, not your exact words. Your sample vault has a note about personal knowledge management that discusses "retrieval" and "conceptual connections." Keyword search for "finding related ideas" would miss it entirely.

  • Try searching with words that do not appear in any of your notes:
python3 vaultmind.py search "finding related ideas in my notes"

You should see results from notes like personal-knowledge-management.md and machine-learning-basics.md even though neither contains the phrase "finding related ideas."

Why does this work?

Supermemory converted your notes into vector embeddings that capture semantic meaning. "Finding related ideas" and "links between ideas are more valuable than folders" point in a similar direction in vector space, even though they share almost no words.

Compare this to Obsidian's built-in search (Ctrl+Shift+F). A keyword search for "finding related ideas" would return zero results because that exact phrase does not exist in any note.

  • Now test the --threshold flag to see how it filters low-relevance results:
python3 vaultmind.py search "distributed systems" --threshold 0.7

With a higher threshold, only notes that are highly relevant to distributed systems appear. Lower the threshold to 0.3 and you will see more results with weaker connections.

  • Before you run this final test, predict: will VaultMind find a connection between "sourdough" and "distributed systems"?
python3 vaultmind.py search "consistency and patience in complex systems" --limit 3

You should see your sourdough note ranked alongside your distributed systems note. Both discuss consistency and patience in different contexts. Semantic search found the conceptual bridge that keyword search never could.

Your CLI now has a working semantic search engine. You can query your vault in plain English and get ranked results by meaning. Next up, you will build the feature that makes VaultMind truly unique: discovering hidden connections between notes that are not already linked.

Discover Hidden Connections

Your search command proves Supermemory Local can find notes by meaning. But the search only answers questions you already know to ask.

The real power of a semantic memory layer is surfacing connections you never noticed. Obsidian's graph view shows explicit [[wiki-links]] between notes. VaultMind's discover command will find notes that are semantically related but NOT already linked, revealing hidden patterns in your knowledge base.

In this step, get ready to:
  • Build a discover command that reads a note and uses it as a semantic query.
  • Format hidden connections as a Rich Panel with similarity scores.
  • Add a --suggest-links flag that outputs Obsidian-compatible wiki-link syntax.
Build the discover command

The discover command takes a note file, reads its content, extracts which notes it already links to, then sends that content as a search query to Supermemory. The trick is filtering out anything already connected via wiki-links.

  • In vaultmind.py, find the if __name__ == "__main__": line at the bottom of the file.
  • Add the following discover command above that line:
@cli.command()
@click.argument("note_path", type=click.Path(exists=True))
@click.option("--limit", default=10, help="Number of candidates to check.")
@click.option("--suggest-links", is_flag=True, help="Output as Obsidian [[wiki-links]].")
def discover(note_path, limit, suggest_links):
    """Find hidden connections for a note (semantically related but not linked)."""
    client = get_client()
    filepath = Path(note_path)
    note = parse_note(filepath)

    # Build a set of already-linked note names for filtering
    existing_links = set()
    for link in note["wiki_links"]:
        existing_links.add(link.lower().strip())
        existing_links.add(link.lower().strip() + ".md")

    console.print(f"[bold]Discovering hidden connections for:[/bold] {filepath.name}")
    console.print(f"[dim]Already linked to: {len(note['wiki_links'])} notes[/dim]\n")

    # Search Supermemory using the note body as the query
    results = client.search.memories(
        q=note["body"][:1000],
        container_tag=CONTAINER_TAG,
        search_mode="hybrid",
        limit=limit,
        threshold=0.5,
    )

What does this code do?

The discover command reuses your existing parse_note() helper to read the file and extract its wiki-links. It builds a set of those link names (lowercased, with and without .md) so we can compare against search results later.

The key insight: instead of searching with a typed query like the search command, it uses the note's own body text (first 1000 characters) as the query. This finds notes whose meaning is similar to the source note.

  • Directly below that search call (still inside the discover function), add the filtering logic:
    # Filter out already-linked notes and the source note itself
    hidden_connections = []
    already_linked = 0

    for result in results.results:
        content = result.memory or result.chunk or ""

        # Check if this result matches an existing wiki-link
        is_linked = False
        content_lower = content.lower()
        for link in existing_links:
            if link in content_lower:
                is_linked = True
                break

        # Skip the source note itself
        source_name = filepath.stem.lower()
        if source_name in content_lower[:100]:
            continue

        if is_linked:
            already_linked += 1
        else:
            hidden_connections.append(result)

    if not hidden_connections:
        console.print("[yellow]No hidden connections found. Your notes may already be well-linked![/yellow]")
        return

What does this filtering do?

For each search result, the code checks two things. First, does the result's content mention one of the note's existing wiki-links? If yes, it is already connected. Second, does the result contain the source note's own filename? If yes, it is the note itself and gets skipped.

Whatever survives both filters is a hidden connection: semantically related content that has no explicit link from the source note.

  • Save vaultmind.py and run the discover command to confirm it reads your note:
python3 vaultmind.py discover ./sample-vault/programming-ideas.md

You should see two status lines printed: "Discovering hidden connections for: programming-ideas.md" and "Already linked to: 2 notes". The function searches and filters but has no display code yet, so it exits after printing those lines.

Seeing an error about the command?

Make sure you added the discover function above the if __name__ == "__main__": block. Click commands must be defined before the entry point.

If you see No such file or directory, confirm you are running the command from inside the vaultmind project folder where sample-vault/ lives.

help me debug my discover command

Format the hidden connections output

The filtering logic works, but without formatting code the results stay invisible. You will add a Rich Panel that displays each hidden connection with its similarity score and a content snippet.

  • In vaultmind.py, add the following code directly below the if not hidden_connections: return line (still inside the discover function):
    # Build formatted output for each hidden connection
    panel_content = ""
    link_suggestions = []

    for i, result in enumerate(hidden_connections, 1):
        content = result.memory or result.chunk or ""
        snippet = content[:150] + "..." if len(content) > 150 else content
        score = f"{result.similarity:.2f}"
        panel_content += f"[cyan]{i}.[/cyan] [bold]Score: {score}[/bold]\n{snippet}\n\n"

        # Collect link suggestions from note titles
        title_match = re.match(r"^# (.+\.md)", content)
        if title_match and suggest_links:
            link_name = title_match.group(1).replace(".md", "")
            link_suggestions.append(f"[[{link_name}]]")

    console.print(Panel(panel_content.strip(), title="Hidden Connections", border_style="green"))
    console.print(
        f"\n[bold green]Found {len(hidden_connections)} hidden connections[/bold green] "
        f"[dim]({already_linked} notes were already linked)[/dim]"
    )

What does this display code do?

The loop builds a string of numbered results, each showing a similarity score and a 150-character snippet. The Panel from Rich wraps it in a bordered box titled "Hidden Connections". The summary line tells you how many hidden connections were found versus how many results were already linked.

The link_suggestions list collects potential wiki-link names by matching the # filename.md pattern at the start of each result's content. You will use this in the next substep.

  • Save vaultmind.py.
  • Before you run this, what do you expect to see for programming-ideas.md? It links to distributed-systems and learning-rust. Which other notes in your vault might be semantically related to programming project ideas?
  • Run the discover command:
python3 vaultmind.py discover ./sample-vault/programming-ideas.md

You should see a green-bordered "Hidden Connections" panel listing notes like machine-learning-basics.md, personal-knowledge-management.md, or weekly-review-june.md with similarity scores. These notes relate to programming ideas but were never explicitly linked.

Seeing no hidden connections?

If the command prints "No hidden connections found", your ingested notes may not have finished processing in Supermemory yet. Memory extraction runs asynchronously after the add() call returns. Wait 30 seconds and try again.

If you still see nothing, try lowering the threshold by editing the threshold=0.5 value in the search call to 0.3 temporarily.

help me troubleshoot why discover finds no hidden connections

Add a --suggest-links flag

Finding hidden connections is useful. Making them actionable is better. The --suggest-links flag outputs discovered connections as Obsidian-compatible [[wiki-link]] syntax you can paste directly into your note.

  • In vaultmind.py, find the summary console.print line at the end of the discover function (the one that says "Found X hidden connections").
  • Add the following code directly below it, still inside the discover function:
    # Output wiki-link suggestions when flag is enabled
    if suggest_links and link_suggestions:
        console.print("\n[bold]Suggested links to add:[/bold]")
        for link in link_suggestions:
            console.print(f"  {link}")

What does this do?

When the learner passes --suggest-links, the code prints any discovered connections that had identifiable note titles as ready-to-paste [[wiki-link]] syntax. The suggestions were already collected in the display loop above. This block just prints them.

  • Save vaultmind.py and run the command with the flag enabled:
python3 vaultmind.py discover ./sample-vault/programming-ideas.md --suggest-links

You should see the same Hidden Connections panel as before, followed by a "Suggested links to add" section listing wiki-links like [[machine-learning-basics]] that you could paste into your note.

No suggested links appearing?

The suggestion logic relies on each result's content starting with # filename.md. This pattern comes from how the ingest command formatted content as f"# {relative_path}\n\n{note['body']}". If Supermemory's memory extraction rewrote the content, the title line may not be preserved in the result.

The Hidden Connections panel still works correctly even without suggestions. The suggestions are a best-effort feature.

help me understand why --suggest-links shows no suggestions

✔️ Awesome, I've got everything!

Great. Double check you have saved vaultmind.py with all the discover command code.

ⓧ I'd like to double check the full code

Your vaultmind.py is now over 30 lines, so here is a checklist. Scroll up through your file and confirm these sections are present in order:

  • Imports and module-level constants (SUPERMEMORY_API_KEY, SUPERMEMORY_BASE_URL, CONTAINER_TAG).
  • get_client(), parse_note(), and get_md_files() helper functions.
  • The cli Click group.
  • The ingest command.
  • The search command.
  • The discover command with: Click decorators (including --limit and --suggest-links), link extraction, search call, filter loop, Panel output, and suggest-links output.
  • The if __name__ == "__main__": cli() entry point at the bottom.

Your discover command surfaces knowledge connections that keyword search and manual linking would never find. Next up, you will tackle the Secret Mission and turn VaultMind into a conversational assistant that synthesizes answers from your vault's memories.

Secret mission

Add a Conversational "Ask" Command

Your search command returns raw results. What if VaultMind could synthesize an actual answer from your notes, citing which memories it drew from? In this mission, you will add an ask command that sends retrieved memories to Gemini and gets back a coherent, sourced response. Your notes will talk back to you.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your machine. Supermemory Local is free and open-source, and Gemini API calls used the free tier. There are no ongoing costs to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Supermemory Local server (running process on port 6767).
  • vaultmind project directory on your Desktop (includes virtual environment, sample vault, and source code).
  • Supermemory Local data (stored in ./.supermemory/ and ~/.supermemory/env).
  • Gemini API key (in Google Cloud API Credentials).

Keep everything running

No action needed. Choose this if you plan to keep developing VaultMind or want to use it regularly with your real Obsidian vault.

  • Your Supermemory Local server will keep running until you close the terminal or restart your machine.
  • The vaultmind project directory remains on your Desktop.
  • Your Gemini API key stays active in Google AI Studio for future projects.
  • The Supermemory Local binary remains installed on your machine (there is no documented uninstall command for it).

Pause - I'll come back to this later

Stop the running server to free up memory and the port, but keep your files so you can resume later.

  • Switch to the terminal running Supermemory Local and press Ctrl+C to stop the server.
  • Your vaultmind directory and all ingested memories remain on disk.
  • To resume later, run supermemory-server again, activate your virtual environment, and re-export your environment variables.

Delete - I don't want to use this again

Remove all project files, stored memories, and revoke your API key.

  • Switch to the terminal running Supermemory Local and press Ctrl+C to stop the server.
  • Delete the project directory by running:
rm -rf ~/Desktop/vaultmind
  • Delete the Supermemory Local data and configuration by running:
rm -rf .supermemory
rm -rf ~/.supermemory
  • Delete your Gemini API key:
  • Open the Google Cloud API Credentials page at https://console.cloud.google.com/apis/credentials.
  • Find the API key you created for this project and click the Actions icon (three dots).
  • Select Delete API key.
  • In the confirmation modal, click Delete. The key will stop working after a few minutes of propagation.

Nice Work!

Nice Work!

Nice work! You just built VaultMind, a fully working CLI tool that turns any Obsidian vault into a semantically searchable knowledge base powered by Supermemory Local.

You've learned how to:

  • Built a Python CLI tool that parses markdown files with frontmatter and wiki-links, then ingests them into Supermemory Local's semantic memory engine for AI-powered retrieval.
  • Implemented hybrid semantic search that finds notes by meaning rather than exact keywords, proving that natural-language queries can surface results that Obsidian's built-in search would miss.
  • Built a discover command that filters out already-linked notes from search results, surfacing hidden conceptual connections that only a semantic similarity layer can find.
  • Secret Mission: Added a conversational ask command that retrieves relevant vault memories and sends them to Gemini as context, synthesizing a direct answer with source attribution.

Ready to quiz yourself?