Metadata-Version: 2.4
Name: locram
Version: 1.3.0
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** — 25 tools for LLM agents: create, read, update, delete, link, search, analytics
- **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          # SQLite — source of truth
└── vault/             # Obsidian vault — human-readable
    ├── .obsidian/     # Auto-configured by locram init
    │   ├── app.json
    │   ├── types.json
    │   ├── templates.json
    │   ├── core-plugins.json
    │   └── graph.json
    ├── templates/
    │   └── locram note.md
    ├── attachments/
    └── *.md           # Notes (filename = title)
```

Override location: `export LOCRAM_HOME=/your/path`

---

## Installation

### Option A — from source (recommended for now)

Requires [uv](https://docs.astral.sh/uv/).

```bash
git clone https://github.com/hazov/locram
cd locram
uv sync
```

Run any command without activating the venv:

```bash
uv run locram init
uv run locram serve
```

Or install as a global tool so `locram` works anywhere:

```bash
uv tool install .
locram init
```

### Option B — from PyPI

```bash
# Recommended: uv tool (manages its own venv, registers locram globally)
uv tool install locram

# Or: pipx (same idea, different tool)
pipx install locram

# Or: plain pip inside a venv
python3 -m venv ~/.venvs/locram
source ~/.venvs/locram/bin/activate
pip install locram
```
 
> **macOS / Homebrew Python users**: plain `pip install locram` outside a venv will
> be blocked by PEP 668. Use `uv tool install` or `pipx install` — they handle
> the isolation automatically and register the `locram` command globally.
>
> **First time using `uv tool`?** If you see _"`~/.local/bin` is not on your PATH"_,
> run `uv tool update-shell` once, then open a new terminal. This is a one-time
> setup for `uv` itself, not specific to locram.

Then:

```bash
locram init
```

### Option C — with embedding support (optional)

```bash
pip install "locram[embeddings]"
```

This adds `sqlite-vec` for Phase 2 vector search. Not required for core functionality.

---

## Quick Start

```bash
# 1. Initialize DB and vault
locram init

# 2. (macOS) Install auto-watcher for Obsidian → DB sync
locram watch-install

# 3. Create your first note
locram create --title "Transformer architecture" --type permanent --subject ai

# 4. Search
locram search "attention"

# 5. Open vault in Obsidian
open ~/.locram/vault
```

---

## MCP Setup

### Cursor

Add to `.cursor/mcp.json` (project-level) or `~/.cursor/mcp.json` (global):

**If installed via `uv tool install locram` or `pip install locram`:**

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

**If running from source:**

```json
{
  "mcpServers": {
    "locram": {
      "command": "uv",
      "args": ["--directory", "/absolute/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 25 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`.

**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"]
    }
  }
}
```

Find the exact path with `which locram` in your terminal.

> **Important**: Claude Desktop launches processes with a restricted PATH that does not
> include `~/.local/bin`. Using `"command": "locram"` without a full path will fail
> with "No such file or directory". Always use the absolute path to `uv` or `locram`.

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 Reference

### Core CRUD

| Tool | Description |
|------|-------------|
| `create_page` | Create a note. Params: `title`, `content`, `type`, `subject[]`, `tags[]`, `parent_id`, `review_interval_days` |
| `get_page` | Get note + full graph context. `identifier`: page_id **or** exact title. Ambiguous title → error with hint to use page_id |
| `update_page` | Partial update — only provided fields change |
| `delete_page` | Soft delete by default (`status=to_delete`). `hard=true` for permanent deletion |
| `list_pages` | List with filters: `status`, `type`, `subject`, `tag`, `since_days` (last N days), `limit`, `offset` |
| `search` | FTS5 BM25 full-text search across title and content |

### Partial Editing

| Tool | Description |
|------|-------------|
| `replace_in_page` | Find-and-replace exact text fragment in note content |
| `update_section` | Replace a Markdown section identified by heading |

### Links & Hierarchy

| Tool | Description |
|------|-------------|
| `link_pages` | Create typed directional link. Auto-creates inverse. `link_type`: see table below |
| `unlink_pages` | Remove link(s). `link_type=null` removes all links between the pair |
| `set_parent` | Set or clear vertical parent (sub-item hierarchy) |
| `batch_link` | Create multiple links: `[{source_id, target_id, link_type}]` → `{created, skipped, errors}` |

### Graph Analytics

| Tool | Description |
|------|-------------|
| `find_orphaned_notes` | Notes with zero links and no parent — candidates for connecting |
| `find_central_notes` | Most-linked notes — hub ideas, sorted by degree |
| `find_similar_notes` | FTS5 BM25 similarity to a given note |
| `find_due_for_review` | Notes past their review date — for night agent maintenance |
| `get_graph_summary` | Stats + topology: page count, type distribution, all pages and links (hard limit: 500) |

### Lifecycle

| Tool | Description |
|------|-------------|
| `promote_page` | Maturity promotion: `fleeting → note-taking → permanent`. Cannot demote. Cannot promote `structure`/`hub` (use `update_page` for those) |
| `mark_reviewed` | Mark note as reviewed. Resets review countdown without editing content |

### Attachments

| Tool | Description |
|------|-------------|
| `get_attachment` | Read file from `vault/attachments/` as base64 |
| `save_attachment` | Save base64 file to `vault/attachments/` |

### Embeddings

| Tool | Description |
|------|-------------|
| `store_embedding` | Store a float32 vector for a page field (`content` / `meta` / `summary`). UPSERT — replaces previous vector for same field |
| `find_unembedded` | List page IDs without a `content` embedding — queue for your embedding agent |

---

## CLI Reference

```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 output 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` (use `promote_page`).
`structure` and `hub` are classification types, not maturity stages — set them via `update_page`.

---

## Link Types

12 semantic link types, directional with automatic inverse creation:

| 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) |

When you call `link_pages(A, B, "extends")`, locram automatically creates both:
- `A → extends → B`
- `B → extended_by → A`

---

## Graph Model

locram uses two orthogonal connection axes:

```
                 Parent page
                 ├── Sub-item 1    ← vertical (parent_id / set_parent)
                 └── Sub-item 2

 ↔ Connected A ←────────────────→ Connected B   ← horizontal (link_pages)

 Content: "See also [[Some Other Note]]"         ← inline mention (derived, not stored)
```

- **Vertical** (`parent_id`): decomposition hierarchy. A sub-item has exactly one parent.
- **Horizontal** (typed links): peer-level semantic connections.
- **Inline mentions**: `[[Title]]` syntax in note body. Resolved at read time in `get_page` — not stored as links.

`get_page` returns all three in a single call:

```json
{
  "id": "...",
  "title": "Attention Mechanism",
  "parent": {"id": "...", "title": "Transformer Architecture"},
  "sub_items": [{"id": "...", "title": "Self-Attention"}],
  "connected_to": [{"id": "...", "title": "BERT", "link_type": "extends", "direction": "outgoing"}],
  "inline_mentions": [{"id": "...", "title": "Query-Key-Value"}]
}
```

---

## Vault Sync

### DB → Vault (write-through)

Every write via MCP or CLI immediately creates/updates the `.md` file.
Frontmatter structure:

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

Note content here. Use [[Other Note]] for inline references.
```

Title = filename (not in frontmatter). Rename the file in Obsidian to change the title.

### Vault → DB (sync)

Without the watcher, vault → DB sync is **manual**. Run `locram sync` after editing in Obsidian, or install the watcher once with `locram watch-install` (macOS only).

Triggered by `locram sync` or automatically via the macOS watcher.

```bash
locram sync              # Full-vault reconcile (scans all .md files, detects deletions)
locram sync --file PATH  # Single-file sync (faster, no deletion detection)
```

**What sync does per file:**
1. Parse frontmatter + content
2. New file without `id` → assign ULID, rewrite file
3. Compare all vault-writable fields against DB
4. Any field changed → UPDATE. Nothing changed → skip (no-op, no sync loop)

**Vault-writable fields**: `title` (filename), `content`, `type`, `status`, `subject`, `tags`, `review_interval_days`

**DB-authoritative fields** (never overwritten by sync): `id`, `parent_id`, `created_at`, `reviewed_at`, `links`, `embeddings`

### Obsidian Auto-Watcher (macOS)

```bash
locram watch-install    # Installs launchd agent — fires locram sync on vault changes
locram watch-uninstall  # Removes it
```

Uses macOS `launchd WatchPaths` with 5-second debounce. Logs: `~/.locram/watcher.log`.

---

## Note Frontmatter Reference

| Field | Editable in vault | Notes |
|-------|-------------------|-------|
| `type` | ✅ | `fleeting` / `note-taking` / `permanent` / `structure` / `hub` |
| `subject` | ✅ | YAML list, e.g. `[ai, llm]` |
| `tags` | ✅ | YAML list, e.g. `[research, todo]` |
| `status` | ✅ | `active` / `archived` / `to_delete` |
| `review in` | ✅ | Days until next review |
| `reviewed` | 🔒 display only | Set by `mark_reviewed` MCP tool |
| `created` | 🔒 display only | Set once on creation |
| `updated` | 🔒 display only | Set by DB on each update |
| `parent` | 🔒 display only | Use `set_parent` MCP tool to change |
| `id` | 🔒 never edit | Sync key (ULID) |

---

## Development

```bash
git clone https://github.com/hazov/locram
cd locram
uv sync --dev            # installs pytest, ruff, build, twine

# Run tests
uv run pytest tests/ -v

# Lint
uv run ruff check locram/

# Run MCP server locally
uv run locram serve
```

### Project Structure

```
locram/
├── models/         # Domain layer — pure Python, no I/O
│   ├── enums.py    # PageType, PageStatus, LinkType, LINK_INVERSES
│   ├── page.py     # Page dataclass
│   └── link.py     # Link dataclass (frozen)
├── interfaces/     # ABCs: PageRepository, VaultStore, EmbeddingStore
├── services/       # Business logic: PageService, LinkService, SearchService, GraphService
├── adapters/       # SQLite implementations
│   ├── db.py
│   ├── sqlite_page_repo.py
│   └── sqlite_embedding_store.py
├── vault/          # Obsidian vault infrastructure
│   ├── writer.py   # DB → .md write-through
│   ├── reader.py   # .md → dict parsing
│   ├── sync.py     # Vault → DB bidirectional sync
│   ├── obsidian.py # Auto-setup Obsidian config
│   ├── attachments.py
│   └── watcher.py  # macOS launchd plist
├── server.py       # MCP composition root
├── cli.py          # CLI composition root
└── schema.sql      # Single clean schema (no migrations)
```

### Test Suite

```bash
uv run pytest tests/ -v
# 78 tests across 7 files
```

```
tests/
├── conftest.py             # Shared fixtures (tmp DB, vault, services)
├── test_models.py          # Domain model — zero I/O
├── test_sqlite_repo.py     # CRUD, links, FTS5, analytics
├── test_link_service.py    # Bidirectional logic, inverse map, promote_page
├── test_graph_service.py   # Orphaned, central, due_for_review, graph_summary
├── test_vault_sync.py      # Write-through, sync roundtrip, recursive vault scan, deletion detection
└── test_integration.py     # End-to-end via services, inline_mentions, get_page by title, since_days
```

---

## Publishing to PyPI

The name `locram` is available on PyPI. To publish:

### 1. Prerequisites

```bash
uv sync --dev   # build and twine are already in [dependency-groups] dev
```

### 2. Create a PyPI account

Register at [pypi.org](https://pypi.org/account/register/) and create an API token at
`Account settings → API tokens`.

### 3. Add your GitHub repository URL to `pyproject.toml`

```toml
[project.urls]
Homepage = "https://github.com/hazov/locram"
Repository = "https://github.com/hazov/locram"
"Bug Tracker" = "https://github.com/hazov/locram/issues"
```

### 4. Build

```bash
uv run python -m build
# produces dist/locram-1.2.0.tar.gz and dist/locram-1.2.0-py3-none-any.whl
```

### 5. Upload

```bash
# Test first (recommended)
uv run twine upload --repository testpypi dist/*

# Then production
uv run twine upload dist/*
```

Enter your API token when prompted (username `__token__`, password = the token).

### 6. Install from PyPI

After publishing:

```bash
pip install locram
# or
uv tool install locram
```

### Notes on PyPI readiness

- `pyproject.toml` is already configured with `hatchling`, classifiers, and keywords
- `README.md` is the package description (set via `readme = "README.md"`)
- `locram = "locram.cli:main"` entry point is registered — `locram` CLI works after `pip install`
- Optional `[embeddings]` extra is declared — users who need vector search install `pip install "locram[embeddings]"`

---

## Roadmap

| Phase | Status | Description |
|-------|--------|-------------|
| Phase 1 | ✅ Done | Core: MCP + CLI + SQLite + Obsidian vault + FTS5 + typed links |
| Phase 2 | 🔜 Planned | Semantic search: `sqlite-vec` ANN index + `semantic_search` MCP tool + `hybrid_search` (FTS5 + vector, RRF) |

---

## Requirements

- Python 3.11+
- macOS / Linux (watcher is macOS-only; sync works everywhere)
- No external services required

---

## License

MIT
