Metadata-Version: 2.4
Name: locram
Version: 1.3.7
Summary: Local Zettelkasten for LLM agents — SQLite + Obsidian vault + MCP
Project-URL: Homepage, https://github.com/nocrun/locram
Project-URL: Repository, https://github.com/nocrun/locram
Project-URL: Bug Tracker, https://github.com/nocrun/locram/issues
Author: nocrun
License-Expression: MIT
Keywords: knowledge-base,llm,mcp,obsidian,zettelkasten
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Text Processing :: General
Requires-Python: >=3.11
Requires-Dist: click>=8.1.0
Requires-Dist: mcp[cli]>=1.3.0
Requires-Dist: python-frontmatter>=1.1.0
Requires-Dist: python-ulid>=3.0.0
Provides-Extra: embeddings
Requires-Dist: sqlite-vec>=0.1.6; extra == 'embeddings'
Description-Content-Type: text/markdown

# locram

**Local Zettelkasten knowledge base for LLM agents and humans.**

SQLite as the source of truth. Obsidian-compatible Markdown vault as the human-readable mirror. MCP interface for any AI agent.

Write notes from Cursor, Claude Desktop, OpenAI Codex, or your own agent. Read them back in Obsidian. No server, no cloud, no account. Everything lives in `~/.locram/`.

---

## What is this?

locram is a personal knowledge base built on two principles:

1. **SQLite is the source of truth.** All notes, links, and metadata live in a single local database. Fast FTS5 full-text search, graph analytics, typed relationships — all queryable in microseconds.

2. **Vault is the human interface.** Every note is simultaneously a Markdown file in `~/.locram/vault/`. Edit in Obsidian. Changes sync back to the DB automatically via a macOS launchd watcher or `locram sync`.

The result: AI agents get a precise, fast, queryable API. Humans get a beautiful Obsidian graph. Both work on the same data.

---

## Key Features

- **MCP server** — 24 tools for LLM agents: create, read, update, delete, link, search, analytics, Mermaid rendering
- **Typed links** — 12 semantic link types with auto-inverse (e.g. `extends` ↔ `extended_by`)
- **Bidirectional sync** — DB → vault on every write; vault → DB via `locram sync`
- **Obsidian-native** — auto-configures `.obsidian/` with graph settings, property types, templates
- **FTS5 full-text search** — BM25 ranking over title + content
- **Graph analytics** — orphaned notes, central hubs, review queue
- **Embedding storage** — stores vectors from external agents for Phase 2 semantic search
- **CLI** — every operation available from terminal, JSON output
- **Zero external services** — runs entirely offline

---

## Data Directory

```
~/.locram/
├── locram.db
└── vault/
    ├── .obsidian/     # created/updated by locram init
    ├── templates/
    ├── attachments/
    └── *.md
```

Override: `export LOCRAM_HOME=/path`

---

## Install

```bash
uv tool install locram
# or: pipx install locram
locram init
```

macOS system Python may block global `pip install` (PEP 668); prefer `uv tool` / `pipx`.

Optional: `pip install "locram[embeddings]"` (adds `sqlite-vec` for future vector search).

---

## Quick start

```bash
locram init
locram watch-install    # macOS: auto sync after vault edits
locram create --title "Hello" --type fleeting
locram search "Hello"
```

### Obsidian

1. Install [Obsidian](https://obsidian.md/).
2. **Open folder as vault** and choose the locram vault directory:
   - default: **`~/.locram/vault`**
   - or: **`$LOCRAM_HOME/vault`** if you set `LOCRAM_HOME`.
3. Notes and the **graph** use that folder; `.obsidian/` is already configured by `locram init`.

Opening the folder in Finder (`open ~/.locram/vault`) does not open Obsidian—you pick the same path inside Obsidian via **Open folder as vault**.

---

## MCP config

**Cursor** — `.cursor/mcp.json` or `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "locram": {
      "command": "locram",
      "args": ["serve"]
    }
  }
}
```

From repo: `"command": "uv", "args": ["--directory", "/abs/path/to/locram", "run", "locram", "serve"]`

> **Using the locram MCP already running in Cursor?** If Cursor already has locram connected as an MCP server, all 23 tools are immediately available to any AI agent in that workspace. No additional setup needed — the agent can call `create_page`, `search`, `get_page`, etc. directly.


### Claude Desktop

Edit `~/Library/Application Support/Claude/claude_desktop_config.json`.

**Claude Desktop** often lacks `~/.local/bin` on PATH—use the full path from `which locram` (or full path to `uv`).

**Option A — from source (recommended):**

```json
{
  "mcpServers": {
    "locram": {
      "command": "/opt/homebrew/bin/uv",
      "args": ["--directory", "/absolute/path/to/locram", "run", "locram", "serve"]
    }
  }
}
```

**Option B — installed via `uv tool install locram`:**

```json
{
  "mcpServers": {
    "locram": {
      "command": "/Users/YOUR_USERNAME/.local/bin/locram",
      "args": ["serve"]
    }
  }
}
```

Restart Claude Desktop after editing the config.

### OpenAI Codex / other MCP-compatible clients

Any client that supports MCP stdio transport can connect:

```json
{
  "mcpServers": {
    "locram": {
      "command": "locram",
      "args": ["serve"]
    }
  }
}
```

### Custom data directory

```json
{
  "mcpServers": {
    "locram": {
      "command": "locram",
      "args": ["serve"],
      "env": {
        "LOCRAM_HOME": "/Users/you/notes"
      }
    }
  }
}
```

---

## MCP tools

### CRUD

| Tool | Description |
|------|-------------|
| `create_page` | `title`, `content`, `type`, `subject[]`, `tags[]`, `parent_id`, `review_interval_days` |
| `get_page` | `identifier` = page id or **exact** title; ambiguous title → error, use id |
| `update_page` | Partial fields only |
| `delete_page` | Soft default; `hard=true` removes row |
| `list_pages` | `status`, `type`, `subject`, `tag`, `since_days`, `limit`, `offset` |
| `search` | FTS5 BM25 on title + content |

### Partial edit

| Tool | Description |
|------|-------------|
| `replace_in_page` | `page_id`, `old_text`, `new_text` |
| `update_section` | `page_id`, `heading`, `new_content` |

### Links

| Tool | Description |
|------|-------------|
| `link_pages` | Typed link; inverses created except `reference` |
| `unlink_pages` | `link_type=null` removes all links between the pair |
| `set_parent` | `child_id`, `parent_id` / null |
| `batch_link` | `[{source_id, target_id, link_type}]` → `{created, skipped, errors}` |

### Graph

| Tool | Description |
|------|-------------|
| `find_orphaned_notes` | No links and no parent |
| `find_central_notes` | By link degree |
| `find_similar_notes` | FTS using the note’s **title** as query (lexical) |
| `find_due_for_review` | Past review interval |
| `get_graph_summary` | Stats + topology; `limit` default **500** (pages/links capped) |

### Lifecycle

| Tool | Description |
|------|-------------|
| `promote_page` | **MCP only** (not a CLI command). Moves **maturity** one step: `fleeting` → `note-taking` → `permanent`. Call when a note is ready for the next stage. Cannot demote; cannot “promote” to `structure` / `hub` — use `update_page(type=…)` for those **classification** types. |
| `mark_reviewed` | **MCP only.** Sets `reviewed_at` without changing body text |

### Attachments

| Tool | Description |
|------|-------------|
| `get_attachment` | `filename` under `vault/attachments/` → base64 |
| `save_attachment` | `filename`, `data_b64` |
| `render_mermaid` | `name` (stem, no extension), `diagram` (Mermaid text, no fences), `output_format` (`svg` \| `png`, default `svg`). Renders via `mmdc` → `vault/attachments/<name>.<format>`. Returns `{rendered, path, filename, embed}`. Requires `mmdc` on PATH: `npm install -g @mermaid-js/mermaid-cli` |

### Embeddings

| Tool | Description |
|------|-------------|
| `store_embedding` | Float32 blob per `field` (`content` / `meta` / `summary`); UPSERT |
| `find_unembedded` | Page ids missing `content` embedding |

---

## CLI

```bash
locram init                              # Create DB, vault, Obsidian config
locram serve                             # Start MCP stdio server
locram sync [--file PATH]                # Sync vault → DB (full or single file)
locram vault-rebuild                     # Rebuild all .md files from DB
locram watch-install                     # Install macOS launchd watcher
locram watch-uninstall                   # Remove watcher

locram create --title "..." [options]    # Create note (JSON output)
locram get <id>                          # Get note (JSON output)
locram search <query> [--limit N]        # FTS5 search (JSON output)
locram list [--type ...] [--status ...]  # List notes (JSON output)
locram link <src_id> <dst_id> [--type]  # Create typed link

locram reset                             # DELETE ALL DATA, recreate from scratch
```

All commands print **JSON**.

---

## Note types

| Type | Handle | Description |
|------|--------|-------------|
| Fleeting | `fleeting` | Quick raw capture — default type |
| Note-taking | `note-taking` | Structured note being refined |
| Permanent | `permanent` | Finished atomic idea — one idea per note |
| Structure | `structure` | Map of Content — organizes a topic via links |
| Hub | `hub` | Domain entry point — links to structure notes |

**Maturity** (pipeline): `fleeting` → `note-taking` → `permanent` — advance with the MCP tool **`promote_page`** when the note earns the next stage. **Classification** types `structure` and `hub` are not maturity steps; set them with **`update_page(type=…)`** (or `create_page`), not `promote_page`.

---

## Link types

12 directional types; `related` is symmetric; `reference` has no inverse. Pair examples: `extends` / `extended_by`, `supports` / `supported_by`, …

| Forward | Inverse | Relationship |
|---------|---------|--------------|
| `related` | `related` | General connection (symmetric) |
| `extends` | `extended_by` | A builds upon B |
| `supports` | `supported_by` | A provides evidence for B |
| `contradicts` | `contradicted_by` | A challenges B |
| `refines` | `refined_by` | A is a more precise version of B |
| `questions` | `questioned_by` | A raises a question about B |
| `reference` | *(none)* | A cites B as a source (unidirectional) |

- **Vertical:** `parent_id` / `set_parent` (sub-items).
- **Horizontal:** typed links (`link_pages`).
- **Inline:** `[[wikilinks]]` in body — resolved in `get_page`, not stored as link rows.

---

## Sync

**DB → vault:** every MCP/CLI mutation updates SQLite, then overwrites the `.md` file.

**Vault → DB:** `locram sync` (full vault) or `locram sync --file PATH` (one file, no delete detection). Watcher (macOS) runs debounced full sync. Logs: `~/.locram/watcher.log`.

**Compared for “did the file change?”:** body hash, title (from first `#` H1, else filename stem), `type`, `status`, `subject`, `tags`, `review in`.

**From vault into DB:** `title`, `content`, `type`, `status`, `subject`, `tags`, `review_interval_days` — when they differ from the DB, the row is updated and **`updated_at` in the DB is set to now**. So editing a note in Obsidian and syncing **advances `updated`**; you do not set `updated` by hand in YAML.

**Not read from vault YAML into DB:** `id`, `parent_id`, `created_at`, **`updated`**, `reviewed` / `reviewed_at`, links, embeddings. Those stay DB-driven; the file is rewritten from DB after a successful sync or MCP write.

**Content change from vault:** if the synced update includes new **`content`**, **`reviewed_at` is cleared** in the DB (then reflected in YAML on the next write).

---

## Frontmatter reference

| Field | Role |
|-------|------|
| `type`, `subject`, `tags`, `status`, `review in` | Editable in Obsidian; **vault → DB** when sync detects changes |
| `created` | **DB only** — set at creation; YAML is a copy; manual edits are not applied from vault |
| `updated` | **DB only** — mirrors `updated_at`; advances on MCP/CLI updates and on vault→DB sync when something **actually changed**. **Not** bumped by **`mark_reviewed`** alone. **Do not hand-edit YAML** |
| `reviewed` | **DB only** — use MCP **`mark_reviewed`**; vault→DB sync **does not** read `reviewed` from YAML. **Clearing:** updating **content** via MCP or sync clears `reviewed_at` |
| `parent` | **DB only** — use **`set_parent`**; YAML is display |
| `id` | ULID; **never change** |

Example block (shape only):

```yaml
---
type: permanent
subject: [ai]
tags: [research]
status: active
created: 2026-03-21T10:30
updated: 2026-03-21T14:15
review in: 7
reviewed: 2026-03-21T18:00:00Z
parent: "[[Parent Title]]"
id: 01KMXXXXXXXXXXXXXXXXXXXXXX
---
```

---

## Roadmap

| Phase | Status | Notes |
|-------|--------|--------|
| Core + FTS + vault | Done | — |
| Embedding storage | Done | `store_embedding`, `find_unembedded`; vectors from external models |
| Mermaid rendering | Done | MCP tool **`render_mermaid`** — diagram → SVG/PNG in `vault/attachments/`; requires `mmdc` (`npm install -g @mermaid-js/mermaid-cli`) |
| Semantic + hybrid search | Planned | `sqlite-vec` ANN index; MCP **`semantic_search`**; **`SearchService.hybrid_search`** (FTS5 + vector, e.g. RRF) — see [`locram-refactor-plan.md`](locram-refactor-plan.md) |

---

## Requirements

Python 3.11+. Watcher is macOS-only; sync works on Linux too.

---

## License

MIT
