2ndbrain
AI companion for your Obsidian vault with semantic search. A Go CLI, MCP server, and native macOS dashboard share a SQLite index, making your knowledge base searchable by both you and your AI coding assistant. Obsidian stays your editor; 2ndbrain is the engine that indexes, searches, and answers underneath it.
Requirements
macOS only. Both products ship for macOS and nothing else: the release builds darwin for Intel and Apple silicon, the dashboard is a native macOS app, and there are no Windows or Linux builds. The CLI is pure Go and will compile elsewhere, but that is not a supported configuration and platform-specific paths (Obsidian's registry, the process-liveness probe) assume macOS.
Install
brew install --cask apresai/tap/secondbrain
This installs both the 2nb CLI and the SecondBrain macOS dashboard app. If you only need the CLI:
brew install apresai/tap/2nb
Or download from GitHub Releases: the CLI as a tar.gz, and the app as a branded drag-to-Applications .dmg.
The app and its .dmg are both Developer ID-signed, Apple-notarized, and stapled, so it launches with no Gatekeeper prompt — whether installed via the cask or by downloading the disk image directly.
New here? Follow the Quick Start guide for the full walkthrough: app, Obsidian plugin, AI setup, and first search.
Quick Start
The complete walkthrough (macOS app, Obsidian plugin, AI providers, MCP) lives in docs/quick-start.md. The CLI fast path:
# Register your existing Obsidian vault in recents (2nb follows the vault
# Obsidian has open; open it there, or pass --vault to any command)
2nb vault set ~/path/to/your-obsidian-vault
2nb vault create ~/vault # only for a brand-new vault
# Configure AI for semantic search & ask
2nb ai setup
# Index with AI embeddings (safe to run repeatedly)
2nb index
# Search (hybrid BM25 + semantic)
2nb search "authentication"
2nb search "how does auth work" --type adr
# Ask questions (RAG with source citations)
2nb ask "What authentication approach did we choose and why?"
# Check vault health any time
2nb vault
# One-time: enable shell tab-completion (zsh)
2nb completion install
The legacy
2nb initcommand still works (it prints a deprecation notice). Prefer2nb vault create <path>.Homebrew installs shell completions automatically. For manual installs,
2nb completion installdetects your existing completion directory from your zsh config (falling back to~/.zsh/completions/_2nb) and adds the requiredfpathandcompinitblock to it. The config is$ZDOTDIR/.zshrcwhenZDOTDIRis set, otherwise~/.zshrc;--rc <path>points it anywhere else, and--no-rcinstalls only the script and prints the two lines to add by hand. The file is copied to<rc>.2nb-backupbefore its first change. The block is placed before early-return guards and before any existing compinit/fpath setup (e.g. from gcloud or Homebrew), so completions load correctly even when other tools initialize the completion system first. Re-running is safe: the block is replaced in-place, not duplicated. If multiple2nbbinaries are found on PATH, a warning is printed showing each binary's version and which is active.
Features
- Hybrid search — BM25 keyword + vector semantic search with Reciprocal Rank Fusion
- RAG Q&A — Ask questions, get answers with source citations
- MCP server — 22 tools for Claude Code, Cursor, and any MCP client, with live status sidecar files and an observability panel
- Suggest Links — AI finds semantically related documents in your vault and proposes wikilinks to insert
- Polish — AI copy-editor that fixes spelling, grammar, and clarity, repairs broken
[[wikilinks]]to existing notes (case/separator/whitespace/alias drift, never guessing an ambiguous target), and adds grounded new links (never inventing a target). Returns the original and polished text together so any client can show a diff; the Obsidian plugin applies it in one click from the note toolbar, ribbon, command, or right-click menu, with an Undo button - Vault health dashboard — unified panel showing index state, embedding portability, stale docs, and provider reachability with one-click Sync and Re-embed All. Notes edited in Obsidian are re-indexed and re-embedded automatically (a debounced incremental index), so search and AI stay current without a manual rebuild. When an upgrade improves how 2nb chunks or embeds your notes,
vault status(and the app/plugin) prompt you to reindex or re-embed so your existing vault picks up the improvement — always a prompt, never an automatic re-embed - Built-in installer: the dashboard updates the CLI (
brew upgradebehind an Update CLI button) and installs or updates the Obsidian plugin into the bound vault (2nb plugin installbehind an Install/Update button) - AI Clients card: per-client rows for Claude Code, Warp, Claude Desktop, and Codex — each showing whether the skill is installed, whether the 2ndbrain MCP server is configured, and (for Claude Code / Claude Desktop) whether the global-instructions block is present, with a one-click Configure button (runs
2nb setup --client …, backup-safe) so you can wire up any assistant at a glance - AI connection testing — one-click probe of your configured embedding and generation models with live latency
- Incremental re-embed —
2nb indexrebuilds embeddings only for documents whose content hash changed - Git integration (read-only) — Recent Activity panel with per-commit file diffs in the dashboard, plus MCP git tools for AI clients
- Skill files — One command to teach 8 AI coding agents about your vault
- Three AI providers — AWS Bedrock, OpenRouter, Ollama (fully local)
- Schema validation — Typed frontmatter, enum constraints, status state machines
- Wikilinks —
[[target#heading|alias]]with link resolution and graph traversal - Broken-link resolution, no dead ends —
2nb lintand the dashboard's Validation tab don't just flag broken[[wikilinks]]; every finding gets exactly one machine recommendation (suggest-target --verdict, prompt selected by a measured eval — seedocs/link-prompt-eval.md): repair case/separator drift, relink to a "did you mean?" pick (source-note context +--llmre-rank), or remove the dead link when nothing matches confidently (the model's explicit decline is the removal signal). Fix all applies only high-confidence rewrites (drift + high); Remove dead links batches the removal-recommended ones behind a pre-checked confirm; everything else surfaces top 2-3 recommendations for a human pick, with create-the-note always available - Document templates — ADR, runbook, prd, prfaq, postmortem, note with enforced schemas
- Native macOS dashboard — SwiftUI + AppKit companion app for vault health, AI configuration, plugin install, MCP monitoring, and git activity; Obsidian remains the editor
- Local-first — All data on disk as plain markdown in your Obsidian vault.
2nbwrites only a gitignored.2ndbrain/sidecar and never rewrites a note's body except via explicit, user-invoked commands (append,prepend,replace); frontmatter edits viametahave always rewritten files in place.
AI Providers
2ndbrain supports three AI providers for embeddings and generation. Most Bedrock models run through the Converse API (Claude, Nova, Llama, Mistral, and more); partner-hosted frontier models on the newer Bedrock "mantle" plane (openai.gpt-5.5, xai.grok-4.3) run over its OpenAI-compatible REST API and need a Bedrock API key (bearer token).
Bedrock models are identified by their route, id@plane/region, because the same model can be served on both planes and in several US regions with independent entitlement. Grok 4.6 is the clearest case: us.xai.grok-4.6@classic/us-east-1 and xai.grok-4.6@mantle/us-west-2 are different endpoints for the same model.
2nb models discover # walk both planes across us-east-1/us-east-2/us-west-2 (free, cached 24h)
2nb config set ai.generation_model xai.grok-4.6@mantle/us-west-2
2nb models discover lists every route it found. A bare model id still works when the model has only one route; when it has several, 2nb refuses and prints the qualified forms rather than picking one for you, because picking wrong means your queries quietly go to an endpoint your account cannot serve.
Bedrock token precedence: AWS_BEARER_TOKEN_BEDROCK, then ~/.config/2nb/bedrock.json (Settings, Cmd+, or 2nb config bedrock --set), then the macOS Keychain (2nb config set-key bedrock), then SigV4 from ~/.aws. To make the SAVED key win over the env var for 2nb only — handy when AWS_BEARER_TOKEN_BEDROCK serves other tools in your shell — turn on 2nb config bedrock --set --prefer-stored-token (or the matching Settings checkbox). File region, when set, overlays vault ai.bedrock.region. The token is never written into a vault.
Quick Setup (Any Provider)
The setup wizard detects credentials and offers recommended defaults:
2nb ai setup
# → Pick provider (Bedrock / OpenRouter / Ollama)
# → Easy mode: recommended models, or Custom: pick from catalog
# → Tests connectivity, saves config, offers to index
Local AI with Ollama
Run everything locally with no cloud calls:
# Check readiness (models, disk, RAM)
2nb ai local
# Guided setup (installs models, configures vault)
2nb ai setup
# Or configure manually
2nb config set ai.provider ollama
ollama pull embeddinggemma
ollama pull qwen2.5:0.5b
2nb index
AWS Bedrock (Default)
Uses your existing AWS SSO credentials — no new API keys needed:
2nb ai status
2nb config set ai.bedrock.profile my-profile
2nb config set ai.bedrock.region us-west-2
OpenRouter
export OPENROUTER_API_KEY=sk-or-...
2nb config set ai.provider openrouter
2nb index
Model Catalog & Benchmarking
Browse verified models across all providers, test any model, and benchmark your favorites:
# See all verified models with pricing
2nb models list
# Discover vendor catalogs (Bedrock, OpenRouter, Ollama), from the 24h cache
2nb models list --discover
2nb models list --discover --refresh # re-walk the vendor APIs instead
# Check credentials and reachability
2nb models list --status
# Test if a model works before switching (--save adds it to your catalog)
2nb models test amazon.nova-micro-v1:0 --save
2nb models test google/gemma-4-31b-it:free
# Discover and auto-promote all passing models in one step
2nb models list --discover --promote
# Discovery as a verb: cache ages per source, NEW/GONE since your last check
2nb models discover # cached read-through + diff
2nb models discover --refresh # drop the cache, walk the vendor planes live
2nb models discover --add <id> --validate # persist a discovery with its routing, then probe it (cost-gated)
# Benchmark your favorites
2nb models bench fav amazon.nova-micro-v1:0
2nb models bench fav us.anthropic.claude-haiku-4-5-20251001-v1:0
2nb models bench # runs embed/generate/search/rag probes
2nb models bench compare # side-by-side latency leaderboard
2nb models bench history # view past runs
# End-to-end wizard: discover → pick → cost preview → test → save
2nb models wizard # TTY interactive flow
2nb models wizard --json # JSON-event stream (GUI / automation)
2nb models wizard --set-active # also write the chosen models into the vault config
# Estimate before you run
2nb models cost-preview us.anthropic.claude-opus-4-6-v1 --probe bench_rag
# Hide a model from selection dropdowns without removing it from the catalog
2nb models disable cohere.embed-multilingual-v3 --provider bedrock --scope vault
2nb models enable cohere.embed-multilingual-v3 --provider bedrock --scope vault
Models are tiered as verified (tested with 2nb) or unverified (available from vendor, use models test to check). The benchmark suite stores results in .2ndbrain/bench.db for tracking performance over time.
Every catalog entry declares an invoke_strategy (e.g. bedrock_converse, bedrock_invoke_cohere_embed, openrouter_chat) so adding a new model variant doesn't require a code change — a catalog entry with the right strategy is enough. The macOS dashboard's Models tab (AI → Models…, Cmd+Shift+,) is the simple front door: vendor checkboxes with Check all / Uncheck all, one Validate button that probes what your account can actually invoke, an always-visible API-key chip, and Answers/Search pickers over the proven working set. The full catalog browser (provider cards, per-model Test / Set active / Enable / Disable / Discover) lives behind its Full catalog disclosure. Catalog changes written by the CLI propagate to the running GUI via FSEvents without reopening the vault.
CLI Commands
The tables below are the overview; the full per-command reference (every flag, JSON shape, and cross-command invariant) is docs/cli-reference.md.
Commands are organized into groups (2nb --help shows the full list).
Global flags: --json, --csv, --yaml, --format (json/csv/tsv/yaml/raw/md/text; listings also paths/tree; raw/md emit a document body and are refused on a value that has none, text renders anything else as name=value lines for reading, tsv is the one to parse), --porcelain, --vault, --unconfigured (permit a write to a vault Obsidian doesn't know, and without it such a write is refused; only actual writes are gated, so meta --get, bare meta, and daily read on a note that already exists never need it), --copy (also copy output to the clipboard), --verbose (-v for debug logging to stderr and .2ndbrain/logs/cli.log)
Getting Started
Documents
Search & AI
Git (read-only)
Quality
Integration
Import / Export
Configuration
All commands support --json, --yaml, --csv, --tsv for machine-readable output, plus --format raw/md to emit a document body (or any Serialize()-able value) verbatim, --format text for plain text, and (on listings) --format paths/tree and --total. A value with no document body refuses raw/md and points at --json; search and list refuse them up front, so the refusal does not depend on whether the query matched anything. git diff, export-context and skills show are the inverse: their output is a body, so raw/md/text emit it, --json wraps it in a record, and csv/tsv/yaml are refused. --format text renders a list of rows as one line of name=value pairs per row and a single record as name: value lines, for reading rather than parsing; the field names are --json's, including the fields of an embedded record, which text and csv promote to the top level the way --json does. An empty listing (list, stale, tags, aliases, orphans, deadends, unresolved, backlinks, links) is [] under --json and --yaml, and the header row alone under csv/tsv. search and ask are the exception: they carry an envelope object rather than a bare array, so a zero-hit search emits its full {mode, warnings, results} envelope under --json and nothing at all on stdout under yaml/csv/tsv/text, and ask exits non-zero when retrieval finds nothing. In csv/tsv, a cell holding a map, slice or struct (a search row's frontmatter, a lint finding's candidates) is compact JSON with sorted keys rather than Go syntax, and an unset pointer field is an EMPTY cell rather than the Go token <nil>. A command whose whole output is a single scalar (config get, meta --get) emits one plain cell under csv/tsv rather than a quoted JSON string. A command that WRITES (models add/remove/enable/disable/enable-state, metrics clear, config set, vault create/set, skills install/uninstall, mcp install/uninstall/reap) emits a small record naming what changed, so a machine format on any of them is parseable rather than empty or prose. Commands whose machine output is a stream of JSON events (models bench, ai engine pull, ai engine rm) accept only --json and refuse other formats by name. --copy also writes a command's rendered output to the clipboard (macOS pbcopy): read/print, meta --get, and daily copy in their default output; any command run with a machine format copies that output.
Obsidian-CLI syntax compatibility. 2nb accepts obsidian-CLI-style invocations as a drop-in scripting replacement for the headless file/markdown layer. The full mapping table, accepted argument forms, and intentional non-goals live in docs/obsidian-cli-mapping.md. It understands key=value arguments (file=, path=, content=, template=, query=, vault=, old=/new=, etc.), boolean tokens (total, append, overwrite, done/todo), colon-commands (daily:path/daily:append, property:set → meta, tags:rename → tags rename, tag:add/tag:remove → tag add/tag remove, link:unresolved), and command aliases (print → read; fm/frontmatter/properties → meta; files → list; search-content → keyword search; list-vaults/set-default-vault/add-vault → the vault subcommands). file= resolves a target by exact path, then basename/title/alias/shortest-unique suffix (failing loudly on ambiguity); path= is a strict exact path. A free-text search/ask query that contains = is preserved (never parsed as a parameter), and an unrecognized key=value passes through unchanged.
Defaults and search scoring
- Parent-command defaults: running a command group without a subcommand invokes its most-useful read-only action:
2nb ai→ai status,2nb models→models list,2nb git→git status,2nb mcp→mcp status,2nb plugin→plugin status,2nb skills→skills list,2nb config→config show,2nb metrics→metrics show,2nb instructions→instructions configured.--helpstill works on every command. - Similarity threshold: hybrid search drops vector hits whose cosine similarity is below the active threshold so barely-related neighbors stop padding result lists. Resolution order: explicit vault config (
2nb config set ai.similarity_threshold 0.25) > a user calibration saved by2nb models calibrate --save(or2nb models add --similarity-threshold, the only two writers, both of which stamp the rowthreshold_source: user) > per-model recommendation from the builtin catalog > global default0.20. A user-catalog value that is unstamped and identical to the builtin recommendation is treated as a copy, not a calibration, so a catalog contaminated by an oldermodels benchrun heals itself on the next read. Builtin recommendations: Nova-20.25(measured; queries embed with Nova's asymmetricGENERIC_RETRIEVALpurpose, which collapses the cosine scale; see docs/search-tuning.md), Nemotron0.60, nomic-embed-text/Titan-v2/Cohere-embed0.50, mxbai/snowflake/bge-m30.55, all-minilm0.35(all estimated from each model's training objective, so run2nb models calibrateto tune for your vault). Override per-query with2nb search "foo" --threshold 0.35.2nb ai statusshows the active value and which tier supplied it. - Calibration:
2nb models calibratesamples random chunk pairs from your vault, reports the noise-floor cosine distribution (p50/p90/p95/p99), and recommends a threshold. Add--saveto persist it to the per-vault user catalog (or--save --scope globalfor all vaults); it is stamped as yours, and no probe, benchmark or promotion will overwrite or invent one. - Score display:
2nb searchnow shows(rrf=X.XXX, cos=Y.YYY)on each result. Therrfis the Reciprocal Rank Fusion score used for ranking;cosis the raw cosine similarity from the vector channel, which is what you actually want to look at when judging whether a result is relevant. If legitimate matches are being cut, lower the threshold; if noise is slipping through, raise it. - Hybrid weighting:
2nb config set ai.vector_weight 1.5(orai.bm25_weight) biases the RRF fusion toward semantic or keyword recall. Both default to1.0(equal-weight RRF); raiseai.vector_weightto lean on the vector channel,ai.bm25_weightfor exact-term-heavy vaults. - RAG context:
askfeeds the full matching note(s) to the model (parent-document retrieval), so an answer in a section deep in a long note isn't truncated away; a note is only windowed around the matched section when it exceeds the budget. Tune with2nb config set ai.rag_context_budget <runes>(total, default 60000) /ai.rag_note_budget(per note, default 20000).
Portable vaults
A vault is self-describing. Paths in the index are relative, IDs are UUIDs, and embeddings live in the DB as raw float32 BLOBs — you can tar a vault and open it on another machine without any migration step.
If the receiver's current AI provider doesn't match what produced the embeddings, 2nb ai status tells you exactly what's wrong:
Vault Embedding State:
As-embedded: amazon.nova-2-multimodal-embeddings-v1:0 (1024d), 42 of 42 docs
Current cfg: ollama / nomic-embed-text (768d)
Status: DIMENSION BREAK
Action: Vault was embedded with 1024d vectors but current provider produces 768d.
Run `2nb index --force-reembed` or switch provider back.
2nb search and 2nb ask fail loudly (stderr + BM25 fallback) on dimension mismatch instead of silently returning worse results. The macOS app surfaces the same warnings as a yellow banner over search results and the Ask AI panel, and the status bar AI dot turns yellow.
Shipping a vault:
tar czf vault.tar.gz \
--exclude='.2ndbrain/logs' \
--exclude='.2ndbrain/recovery' \
--exclude='.2ndbrain/mcp' \
--exclude='.2ndbrain/bench.db' \
--exclude='.2ndbrain/metrics.db' \
--exclude='.2ndbrain/eval' \
my-vault/
Include .2ndbrain/config.yaml and .2ndbrain/index.db — the receiver gets the vault's as-embedded state and avoids re-embedding from scratch. For git-shared team vaults, 2nb vault create writes a .gitignore that excludes personal/local state (config, DBs, logs, recovery) and commits only schemas.yaml. Missing or corrupt config.yaml / index.db self-heal on next open with a one-line stderr warning — the vault never bricks.
Privacy caveat: embeddings are a lossy reconstruction of the source text, so shipping a vault with embeddings is functionally equivalent to shipping (approximate) content, and the .2ndbrain/eval/ QA cache embeds note bodies verbatim. Strip both when the receiver should not see the content; a --strip-embeddings export mode is future work.
Heads-up for scripters:
2nb search --jsonand2nb ask --jsonnow return envelopes ({mode, warnings, results}/{mode, warnings, answer, sources}; multi-turn asks via--historyaddrewritten_query). If you were parsing a raw array/object, extract.results/.answer.mode,warningsandresults/sourcesare always present and nevernull(rewritten_queryremains optional, appearing only on a rewritten multi-turn ask), and a search that matches nothing still returns the envelope withresults: [], so| jq .resultsis safe on any query. Every other--jsonlisting returns[]rather thannullwhen empty:list,stale,tags,aliases,orphans,deadends,unresolved,backlinks,links, and that holds for their bare and--porcelainforms too.lint --jsonlikewise reportsissues: []on a clean vault.askexits non-zero when retrieval finds nothing, so check the exit code before parsing.
MCP Server
The MCP server exposes 22 tools for AI coding assistants:
move/rename (the wikilink-rewriting vault mutation) is intentionally CLI-only: the highest-blast-radius write stays behind 2nb move/2nb rename with their mandatory --dry-run preview rather than an MCP tool.
Each running 2nb mcp-server writes a sidecar status file to .2ndbrain/mcp/<pid>.json with PID, start time, parent PID, and the last 50 tool invocations. Run 2nb mcp status to list live servers, or use the Cmd+Shift+M status panel in the dashboard.
The easy way: 2nb setup --client claude-code (or claude-desktop, warp, codex, or --all) installs the skill and writes the MCP config for you, backing up any existing file and preserving your other servers. 2nb mcp install --client <name> writes just the MCP entry. The manual snippets below are a fallback. Per-client parameters, server internals, and the full tool table live in docs/mcp-integration.md.
Claude Code
Add to ~/.claude.json:
{
"mcpServers": {
"2ndbrain": {
"command": "2nb",
"args": ["mcp-server"],
"cwd": "/path/to/your/vault"
}
}
}
Cursor
Add to .cursor/mcp.json:
{
"mcpServers": {
"2ndbrain": {
"command": "2nb",
"args": ["mcp-server"],
"cwd": "/path/to/your/vault"
}
}
}
Claude Desktop
2nb setup --client claude-desktop (or mcp install --client claude-desktop) writes this for you. Claude Desktop is a GUI app launched with a minimal PATH, so the command must be an absolute 2nb path and must NOT include a cwd or url field (a url field silently corrupts the file). Restart Claude Desktop to apply.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"2ndbrain": {
"command": "/opt/homebrew/bin/2nb",
"args": ["mcp-server", "--vault", "/path/to/your/vault"]
}
}
}
Codex
2nb mcp install --client codex (or 2nb setup --client codex) runs codex mcp add so Codex manages its own ~/.codex/config.toml — no manual edit. If the codex CLI isn't installed, the command prints the exact codex mcp add line and the [mcp_servers.2ndbrain] TOML to paste.
Grok
Add to ~/.grok/config.toml:
[mcp_servers.twonb]
command = "2nb"
args = ["mcp-server", "--vault", "/path/to/your/vault"]
Name the server twonb, not 2ndbrain. Grok prefixes every tool as <server>__<name> and keeps only names matching ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$, so a key starting with a digit makes all 22 tool names illegal and the session catalogs 0 tools. Grok also imports servers from ~/.claude.json, so the Claude Code entry above hits the same wall; the twonb key fixes both. grok mcp doctor does not apply the prefix and still reports 22, which is why the breakage is easy to miss.
Run 2nb mcp-setup for config snippets for additional tools (Gemini CLI, Amazon Q, Kiro).
Skill Files
Install a skill file to teach AI coding agents about your vault's CLI, MCP tools, and document format:
# See supported agents and status
2nb skills list
# Install for one agent
2nb skills install claude-code
# Install for all supported agents
2nb skills install --all
# Install globally (all projects, not just this vault)
2nb skills install claude-code --user
Supported agents: Claude Code (also serves Claude Desktop, which reads the same ~/.claude/skills), Cursor, Windsurf, GitHub Copilot, Kiro, Cline, Roo Code, JetBrains Junie, Warp, Codex, and the cross-tool agents (.agents/skills/) standard. 2nb setup --client <name> installs the skill and the MCP server in one step; a force-overwrite of an existing skill backs it up first.
macOS Dashboard
A native SwiftUI + AppKit configuration and companion app. It is not an editor: Obsidian stays your editor. The dashboard binds to the vault Obsidian currently has open (read from Obsidian's own registry) and gives you one place to manage everything around it.
Home (the default screen) covers the common cases:
- Vault card: active vault name and path, a badge confirming it matches the vault Obsidian has open, and an Obsidian plugin row showing the installed plugin version with an Install/Update button (runs
2nb plugin install) - AI card: provider and models (AWS Bedrock with Claude Haiku 4.5 + Amazon Nova-2 by default) with a readiness dot, an always-visible link to the AI settings (where the API key lives), a Test button, and a Reset-to-recommended-defaults button that appears only when the config has drifted
- AI Clients card: a summary of how many clients can reach this vault, linking to Settings > Integrations, where each client (Claude Code, Warp, Claude Desktop, Codex) shows skill-installed, MCP-configured, and global-instructions status with a one-click Configure button (runs
2nb setup --client …, backup-safe; Codex without its CLI shows the manualcodex mcp addstep instead of a false success) - Index card: document and embedding counts, a "N notes awaiting embedding" hint, and Sync (incremental, embeds only what changed) and Re-embed All buttons. Notes edited in Obsidian sync automatically
- The app bundles its own version-matched
2nbCLI and prefers it, so its AI/indexing calls never run a stale Homebrew copy. An orange banner only appears on dev builds that fall back to an older PATH2nb; when Homebrew is present it offers an Update CLI button that runsbrew upgrade apresai/tap/twonbto refresh the terminal/plugin's copy
Configuration lives in the Settings window (Cmd+,): General, AI (connection, API key, the two active models, and a Test everything button that calls them for real), Advanced (every tuning knob), and Integrations (which AI tools can reach this vault).
The rest of the sidebar:
- Models: the model catalog with provider cards, active model slots, per-model test and benchmark, and vendor policy (Cmd+Shift+,)
- Notes:
2nb lintfindings with one-click repairs, guided link fixes, and bulk dead-link removal - Testing (Cmd+Shift+T): everything measurable in one place: Validate (which models your account can really invoke, plus a Discover card with per-source cache ages and one-click add/validate of newly listed models), Benchmarks (run one model x probe or the favorites battery, a models x probes compare matrix, the embed-throughput curve, bench history), Performance (
2nb metrics), and Quality (the2nb evalretrieval scorecard, LLM-jury answer grades, and tuning-sweep suggestions, every run cost-previewed before any spend) - Health: Vault (index coverage, embedding portability, AI reachability, stale docs) · Updates (app, CLI, and plugin versions against the latest release, with one-click upgrades)
- Settings: the same four Settings tabs hosted inline, so configuration is reachable without knowing Cmd+,
- Activity: Git (recent commits, 1/3/7/30-day window; click for per-file diffs — Cmd+Shift+G) · MCP Server (live server processes and recent tool invocations — Cmd+Shift+M)
Menus: Vault (New Vault, Open Vault Cmd+Shift+O, Reveal in Finder, Vault Status, Sync Index, Validate Vault, Import/Export Obsidian), View (Recent Activity Cmd+Shift+G), and AI (Models… Cmd+Shift+Comma, Testing & Benchmarks… Cmd+Shift+T, MCP Server Configuration, MCP Server Status Cmd+Shift+M).
Build and install from source:
make install # CLI to /usr/local/bin, app to ~/Applications
Document Types
Documents are plain .md files with YAML frontmatter:
---
id: <UUID>
title: Use JWT for Authentication
type: adr
status: proposed
tags: [auth, security]
created: 2026-04-04T00:00:00Z
modified: 2026-04-04T00:00:00Z
---
# Use JWT for Authentication
Content with [[wikilinks]] to other documents.
A value may be quoted or not: modified: 2026-04-04T00:00:00Z and modified: "2026-04-04T00:00:00Z" are the same instant to 2nb, which matters because Obsidian's own Date property writes the unquoted form while 2nb create and 2nb meta quote theirs. A title, type, status, id, tag or alias written the same way is read too, and it keeps the text your file shows: title: 2026-09-04 is the title 2026-09-04, not a timestamp. Only the date properties are normalized, because stale and list --sort modified compare instants. 2nb reads these values, and a frontmatter-only command touches only the property you named: every other line comes back byte for byte, its spelling, quoting, list style and any comment intact. A note's --- lines must be exactly that, opening and closing (a trailing space, tab or other invisible character is fine); a horizontal rule like ---- is your text, not the end of your properties. A note using Windows line endings keeps them.
Obsidian Compatible
Import an existing Obsidian vault:
2nb import-obsidian ~/my-obsidian-vault
This adds UUIDs, normalizes tags from inline #tag to frontmatter, and builds the search index. Your files stay as plain markdown.
Export back to Obsidian anytime:
2nb export-obsidian ~/export-dir --strip-ids
Development
make build # CLI + macOS app
make install # CLI to /usr/local/bin, app to ~/Applications
make test # Go unit tests
make test-battery # Golden-path E2E battery (vault, CRUD, index, MCP, skills)
make test-swift # Swift unit tests (JSON decoding, parsing, wizard logic)
make test-gui # GUI tests (AppleScript automation)
make test-all # Go + battery + Swift + GUI
Requires Go 1.26+ (the CLI is pure-Go, no CGO); macOS 14+ for the Swift app.
The Go suite runs on every pull request (.github/workflows/ci.yml) with no credentials: tests that need an AI provider skip themselves when one is not reachable, so a green run proves the code, not the maintainer's AWS account. Running make test locally with credentials configured simply runs more of it.
See CONTRIBUTING.md for details.
Releasing
A release ships three products at one version: the 2nb CLI, the Obsidian plugin, and the SecondBrain macOS app. CI builds the first two; the app is signed and notarized locally, so signing keys never enter CI.
make release-all # the front door: test gate, bump, tag, wait for CI, then sign/notarize/publish the app
Or step by step:
# 1. Bump the version
make bump-build # 0.8.0 → 0.8.1 (or bump-minor / bump-major)
# 2. Release: updates CHANGELOG.md, commits, tags, pushes; CI takes it from there
make release
# 3. After CI finishes: build, sign, notarize, and publish the app + cask (local only)
make release-app
GitHub Actions on the tag push:
- Builds CLI binaries (arm64 + x86_64) via GoReleaser
- Builds and uploads the Obsidian plugin assets (
manifest.json,main.js,styles.css,versions.json) - Creates a GitHub Release with the CLI archives and plugin assets
- Pushes the Homebrew formula (
twonb, plus the2nbalias) toapresai/homebrew-tap
make release-app then runs on the maintainer's machine: it builds SecondBrain.app, signs it with a Developer ID certificate (hardened runtime), notarizes via Apple notarytool, and staples the ticket; then builds a branded drag-to-Applications SecondBrain-<version>-arm64.dmg (scripts/make-dmg.sh, via create-dmg) and Developer ID-signs, notarizes, and staples the DMG too (stapling both means the app launches offline even after being dragged out of the image, and the downloaded .dmg itself passes Gatekeeper offline); finally it uploads the DMG to the release and updates the cask in the tap with the new version and sha256. Signing config is read from scripts/sign.env (gitignored; template at scripts/sign.env.example); building the DMG needs create-dmg (brew install create-dmg).
Users install with:
brew install apresai/tap/2nb # CLI
brew install --cask apresai/tap/secondbrain # macOS dashboard app (depends on the CLI formula)
To build locally without GitHub Actions:
make release-local # runs goreleaser locally (CLI only)
make package-app # builds a branded SecondBrain .dmg locally (Developer ID-signed if sign.env present, else ad-hoc; local use only)
Architecture
cli/ Go CLI + MCP server (cobra, modernc.org/sqlite, mcp-go, aws-sdk-go-v2)
app/ Swift macOS dashboard (SwiftUI, GRDB.swift, swift-markdown)
plugins/ Obsidian plugin (thin wrapper that shells out to 2nb)
| |
+-------- shared ----+
.2ndbrain/index.db (SQLite WAL)
The CLI and dashboard share the same SQLite database via WAL mode for concurrent access. All AI operations go through the provider interfaces in cli/internal/ai/.
License
MIT - Copyright (c) 2026 Apres AI