Ease of Maintenance
approvedby nhan
A vault management tool with integrated agentic capabilities that is Self contained inside Obsidian. - This plugin has not been manually reviewed by Obsidian staff.
Heavy work in progress. Until this notice is removed, discretion is advised — do not rely on this plugin for vaults you cannot afford to lose.
Vault Ease of Maintenance
An LLM-driven assistant that keeps an Obsidian vault easy to maintain. It builds a GraphRAG-style search index of the vault's notes, then uses agents to clean notes, sort the inbox, and answer questions. Everything runs inside Obsidian, you do not need Python, a server, or Docker.
Plans & Goals
- Goal — keep growing vaults maintainable: agents tidy messy notes, triage the inbox, and answer questions with cited sources. Nothing changes without your review.
- Retrieval — a GraphRAG-style index: semantic embeddings, an entity/relationship graph, and LLM-written community reports. It is a pragmatic, in-process take on GraphRAG — embeddings stay the primary retrieval signal, and there is no external graph database or clustering service.
- Providers — any OpenAI-compatible API, hosted or fully local. Bring your own key. I will expand to Claude's API in the future.
- Roadmap — the retrieval layer (hybrid search, community reports, global mode, semantic edges) is complete; incremental index builds are wired. Next: agentic capabilities across clean, sort, and build.
Current Features
- Build the index — Builds the GraphRAG-style index: notes are split into sections, each section is embedded, entities and relationships are extracted into a graph, and communities get LLM-written reports. Rebuilds are incremental.
- Clean current note — The cleanup agent proposes edits for the note you have open. Review the diff in the review pane, then accept or reject.
- Sort inbox — The triage agent suggests where each inbox note belongs. Nothing moves without your approval.
- Chat with your vault — Ask a question; the agent answers from the index with cited sources. Models that support tool calls run agentically; others automatically get a deterministic retrieval fallback.
How Retrieval Works (GraphRAG-style)
The index is a hybrid of embeddings and a graph, tuned to run entirely inside Obsidian:
- Embeddings — every note section is embedded; semantic search is a cosine scan over stored vectors.
- Entity graph — wikilinks, backlinks, and LLM-extracted semantic relationships form edges that chat search traverses alongside the embeddings.
- Community reports — notes are grouped into communities; the LLM writes a summary per community, and overview-style questions ("what is this vault about?") are answered from those reports.
It is not canonical Microsoft GraphRAG — there is no Leiden clustering and no external graph store. It is a GraphRAG-style hybrid that keeps embeddings as the primary retrieval signal while adding graph traversal and a global-report mode.
Providers
The indexer and agents speak to any OpenAI-compatible API:
- Hosted — OpenAI, OpenRouter, or any compatible cloud endpoint.
- Local — OMLX, Ollama, LM Studio, vLLM, or any local OpenAI-compatible server.
Bring your own key: set it in the Settings tab, in config.yaml, or via an environment variable (OMLX_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY). Model choice is free — small local models work too, and chat automatically falls back to deterministic retrieval when a model cannot emit tool calls.
Quick Start
1. Install the plugin
Install from the Obsidian community store:
https://community.obsidian.md/plugins/ease-of-maintenance
Store installs work out of the box — the SQLite engine (sql.js, SQLite compiled to WebAssembly) is embedded inside main.js, so there is no native module, no node_modules in the plugin folder, and no extra asset to ship.
2. Set the API key
The indexer and agents call an OpenAI-compatible API. Use OpenAI, OpenRouter, or a local server such as OMLX.
Set the key in one of these places:
- Plugin Settings → API Key.
api.api_keyinconfig.yaml(repo-local, plugin dir).- An environment variable:
OMLX_API_KEY,OPENAI_API_KEY, orOPENROUTER_API_KEY.
The plugin checks these sources in this order.
3. Configure the plugin
The Settings tab is the main configuration and always wins. Resolution order:
code defaults ← <pluginDir>/config.yaml ← Settings tab (MAIN)
- Plugin-store users: configure the Settings tab only (they have no
config.yaml). - Repo users / local dev: copy
config.example.yaml→config.yamlin the plugin folder. - There is no vault-level config file by design — vaults may live in shared/company databases where API keys and parameters must not be stored. Config lives only in the Settings tab and the repo's
config.yaml. Seeconfig.example.yamlfor all options.
4. Build the index
Run the command Build graphrag index. The plugin scans the vault, splits notes into header sections, extracts entities and relationships, builds the graph, writes community reports, and creates embeddings. It stores the index at {vault}/.note-maintainer/index.db.
If no _manifest.md exists, the plugin derives one from the index. Review it before you run sort.
5. Use the agents
- Clean current note — clean the note you have open. The plugin proposes edits. Review the diff in the modal, then accept or reject.
- Sort inbox — triage the inbox. The plugin returns placement decisions. It does not move files without your approval.
- Chat with your vault — ask a question. The plugin answers from the index with cited sources.
Manual install (repo users / local dev)
- Run
npm run buildin the repo root. This writesmain.js(with the sql.js WASM engine embedded inside it) andsql-wasm.wasm(kept as a convenience asset for local installs; not required). - Create the folder
<vault>/.obsidian/plugins/obsidian-vault-ease-of-maintenance/. - Copy
main.js,manifest.json, andstyles.cssinto that folder.sql-wasm.wasmis optional — the engine is embedded inmain.js, so the standard three-file set (exactly what the community store installs) works. Easiest: run./build-plugin.sh <vault-path>(copies all four files). - Enable the plugin in Obsidian: Settings → Community Plugins.
On the first run after upgrading from a version that used the native
better-sqlite3 engine, the plugin retires the old index to
.note-maintainer/legacy/ and rebuilds it once (the index is derived data;
Obsidian notifies you).
Commands
| Command | Purpose |
|---|---|
| Build graphrag index | Full index rebuild from scratch. |
| Clean current note | Run the cleanup agent on the open note. |
| Sort inbox | Run the triage agent on the inbox. |
| Chat with your vault | Ask a question about the vault. The chat agent can ask clarifying questions inline (the clarify tool) and, for manifest-review requests, propose a manifest update with a diff you accept or reject. |
| Understand vault (read it like a book) | Run the vault-comprehension pipeline: skim the vault like flipping through pages, form and score hypotheses in a persistent ledger, verify them against the index, ask for clarification only on deterministic triggers, and write a one-page summary card (.note-maintainer/vault-summary.md) that later sessions load instantly. |
Plugin Settings
| Setting | Purpose |
|---|---|
| Review container | Where clean/sort reviews and chat open: a docked sidebar pane or a centered modal overlay. |
| API Key | Key for the OpenAI-compatible API. Optional when you use an env var. |
| API Base URL | Base URL of the API. |
| Reasoning model | Model for clean, sort, and chat. |
| Reasoning | Let the model think before answering. Applies to local and hosted providers alike. Off is the default. |
| Thinking effort | How much thinking to allow when Reasoning is on (minimal/low/medium/high). Models without thinking levels ignore it. |
| Embedding Model | Model for embeddings. |
| Inbox Folder | Folder to sort. Empty = auto-discover. |
| Ignore Patterns | One glob per line. Skips matching files and folders. |
| Manifest Filename | Name of the vault manifest. Default: _manifest.md. |
| Entity extraction output cap | Most tokens one entity-extraction call may generate. Raise it when the build reports that extraction hit the output cap. |
| Entity extraction batch budget | Tokens of note text per extraction call. Notes batch under this budget — a smaller batch asks for fewer entities, so the model finishes inside the output cap. |
| Community report output cap | Most tokens one community-report call may generate. Raise it when the build reports that reports hit the output cap. |
| Community report context budget | Tokens of member-note text per community report. Higher feeds more evidence into each report for global-mode answers. |
| Comprehension window budget | Estimated tokens of the growing comprehension conversation. Older turns are compacted away beyond it. |
Review UI
Clean, sort, and chat render in native Obsidian views — a docked sidebar pane or a centered modal, per the Review container setting. There is no browser tab and no server.
- Clean review — a modal shows the original and the cleaned content side by side. Accept writes the file and saves a
.bakbackup. Reject keeps the original. - Sort review — a modal lists the placement decisions with scores and destination context.
- Chat — a modal shows the answer and the sources.
Configuration
See config.example.yaml. Key settings:
api.base_url— API endpoint.api.api_key— API key. Optional; falls back to an env var.embedding.model— model for embeddings.embedding.dimensions— dimension count.agent.model— model for agent calls.manifest.filename— manifest file name.preview.enabled— review-before-write for clean.preview.ttl_minutes— how long a pending review stays valid.query.top_k— default result count.reports.context_cap_tokens— per-community context budget for a generated report. Also a Settings-tab control.reports.max_output_tokens— completion cap for one report call. Bounds a single call. Also a Settings-tab control.extraction.context_cap_tokens— per-call budget for entity extraction. Files batch greedily under it. Also a Settings-tab control.extraction.max_output_tokens— completion cap for one extraction call. Bounds a single call. Also a Settings-tab control.comprehension.context_budget_tokens— conversation budget for the comprehension pass. Also a Settings-tab control.index.warn_mb— warn (in the devtools log) when the index file exceeds this size. sql.js holds ~10× the file size in RAM while building, so a large index is also a RAM event.
These keys live in config.yaml, and the five build-side token caps ALSO have Settings-tab rows: reports.max_output_tokens, reports.context_cap_tokens, extraction.max_output_tokens, extraction.context_cap_tokens, and comprehension.context_budget_tokens. Edit one in the Settings tab and it is written to data.json — the last merge layer, so your edit wins over config.yaml from then on. Every other YAML-only key stays out of data.json, which keeps config.yaml authoritative for it. embeddingDimensions is the one exception: it has no Settings-tab row, but it is persisted, because losing a configured value would change the vector width of the index.
Merge order: code defaults ← <pluginDir>/config.yaml ← data.json (wins).
Exclusion
Set the ignore patterns in the plugin Settings tab. The plugin skips matching files and folders during indexing and sorting. The format matches .gitignore.
Security model — vault confinement
Every file operation in the plugin routes through one synchronous confinement layer, src/io/vault_io.ts (VaultIO). It accepts only vault-relative paths and enforces two guards before any I/O:
- Path normalization — absolute paths and parent (
..) traversal are rejected. - Realpath verification — the deepest existing ancestor of a target must resolve at-or-inside the vault root, which defeats symlink escapes (a symlink inside the vault pointing outside).
The plugin writes only inside the vault: the index at .note-maintainer/index.db, pending reviews at .note-maintainer/pending, the sort journal at .note-maintainer/sort-journal.jsonl, the active chat session at .note-maintainer/chat/session-*.jsonl (one per chat tab, deleted when the tab closes), .bak backups beside edited notes, and atomic .tmp-* files beside their targets. Plugin settings are stored by Obsidian via loadData.
The index is read and written through Obsidian's vault adapter (app.vault.adapter), never through a native module or a raw path. The sql.js engine runs inside a disposable Web Worker that is spawned per GraphRAG execution and terminated when the execution finishes — the worker frees the WASM heap, which sql.js never shrinks in-process.
Version control (git-managed vaults)
All generated data lives under the single .note-maintainer/ directory, so one ignore rule keeps it out of version control:
.note-maintainer/
Also consider ignoring Obsidian's own per-user state if your vault is committed or shared: .obsidian/workspace.json, .obsidian/workspace-mobile.json, and cache directories. The plugin's Settings-tab config (data.json under .obsidian/plugins/obsidian-vault-ease-of-maintenance/) may hold personal API settings — for shared/committed vaults, ignore it and keep secrets in your own config.yaml (see Configuration) or environment variables instead.
Chat modes — automatic tool-call detection
The chat agent auto-detects at startup whether the configured model can emit tool calls (one tiny probe call, cached per model):
- Agentic mode — the model calls
search_index/cite_sourceitself. Used when tool calling is detected. - Retrieval fallback mode — models that cannot call tools (small/quantized models) still get grounded answers: the plugin embeds your question and scans the index deterministically, and the model only writes an answer over the retrieved notes. You are notified once at startup which mode is active; a failed probe (model unreachable, fresh install) stays silent.
Watching a long build
A cold build runs three stages in order: comprehension, manifest population, and the index build. All three report progress on the same chat line.
Comprehension (the slowest prefix, and the part that used to be silent):
Comprehension: turn 3/40 (12/60 tool calls).
Comprehension: skim → 13/60 tool calls.
Index build, phase 2 (the only stage that waits on the network):
- A line lands before each LLM call —
Enrichment: entity extraction 3/14 — calling <model> (28 sections, ~2.9k tokens)…— so the first slow response is never a silent gap. - While that call is in flight the same line keeps ticking —
… — still waiting (25s)…— every 5 seconds. A ticking line means alive; a frozen one means hung. - Each completed call appends its own line (
Enrichment: entity extraction 3/14.), and the run ends withEnrichment done: N communities in Xs. - If a response hits
max_output_tokens, a permanent line reports it:Enrichment: entity extraction 3/14 hit the output cap — the incomplete final line was dropped (raise extraction.max_output_tokens if this repeats).
For exact timings, open the developer console (Ctrl/Cmd+Shift+I → Console) and filter on build-debug. It prints a timestamped line for each phase boundary, extraction batch, community report, and HTTP attempt (HTTP 200 after 9412ms). Diagnostics are temporary and gated by DEBUG_LOGGING in src/debug.ts — set it to false to silence them.
If a single call hangs, it fails after DEFAULT_REQUEST_TIMEOUT_MS (10 minutes) instead of waiting forever, and the message tells you so. The core index written in phase 1 stays on disk either way.
Important Files
| Path | Role |
|---|---|
main.ts | Plugin entry. Registers commands and settings; the settings tab renders every row through renderImperativeSetting / toSettingDefinition, with the bounded token-cap fields sharing renderNumberSetting. |
src/config.ts | Settings types and API key resolution. |
src/indexer/scanner.ts | Scans the vault for markdown files. |
src/indexer/chunker.ts | Splits notes into header sections. |
src/indexer/embedder.ts | Calls the embeddings API. |
src/indexer/entity_extractor.ts | Extracts wikilinks, tags, and phrases. |
src/indexer/db.ts | Async facade — the only DB entry point (sql.js + disposable worker), including the phase-1 checkpoint() write. |
src/indexer/db_worker/ | sql.js engine, typed worker protocol, and the worker bundle. |
src/indexer/db_host.ts | Main-thread host: vault-adapter I/O, browser worker, wasm loading. |
src/indexer/manifest.ts | Parses _manifest.md. |
src/indexer/indexer.ts | Orchestrates the two-phase indexing pipeline: a core index (chunk, embed, edges, communities) checkpointed to disk, then non-fatal LLM enrichment. |
src/indexer/completion_output.ts | Build-side completion handling — detects an output-cap truncation (finishReason === "length") and drops the incomplete final line before the parser sees it. |
src/agent/engine.ts | Deterministic primitives: file registry, validators, journal, receipts. |
src/agent/conversation.ts | Shared conversation store (chat + clarify namespaces, bounded history). |
src/agent/clarify.ts | Portable clarification harness: read manifest, detect uncovered folders, ask (injectable question source), propose ops, diff, guarded write. |
src/agent/tools.ts | Agent tools, including apply_edits, the clarify tool, and the withClarify compose helper. |
src/chat-review.ts | Chat renderer — message list, in-flight answer mode for clarify calls, the manifest diff accept/reject card, and the live build-progress line (one transient element rewritten in place). |
src/comprehension/skim_format.ts | The dense one-line-per-note skim report the model reads as a tool result. |
src/comprehension/progress.ts | Chat progress lines for the comprehension pass (turn and tool-call counts). |
src/progress.ts | Shared progress helpers — elapsed/format math plus the phase-2 heartbeat that keeps an in-flight LLM call reporting itself. |
src/settings/persist.ts | Builds the data.json payload — only the keys the Settings tab renders, so config.yaml keeps authority over the YAML-only tuning. |
src/settings/schema.ts | The settings schema: PluginSettings, DEFAULT_PLUGIN_SETTINGS, the one SETTING_META table that drives both renderers (including the five numeric token-cap rows), PERSISTED_SETTING_KEYS, parseTokenCap (bounds TOKEN_CAP_MIN/TOKEN_CAP_MAX/TOKEN_CAP_STEP), and normalizeSettingValue. |
src/settings/nested.ts | pluginSettingsToNested plus resolveEmbeddingDimensions — the single flat-to-nested projection used by BOTH plugin startup and Settings-tab edits, so no settings section can be wired into one path and forgotten in the other. |
src/debug.ts | TEMPORARY build diagnostics (debugLog, DEBUG_LOGGING) — timestamped console lines for phase boundaries, LLM batches, and HTTP attempts. Set the flag to false to silence. |
src/io/vault_io.ts | Vault-confined sync file layer — the only place fs appears. |
src/agent/llm_client.ts | API transport for local, OpenAI, and OpenRouter providers. |
src/agent/llm.ts | Chat loop with tool calling. |
src/agent/runtime.ts | Orchestrators: build, clean, sort, chat. |
src/preview/pending.ts | Stores pending review state. |
tests/ | Vitest suite. |
When to Rebuild
If you've cloned this repo and looking to manually build the project, run the follow ./build-plugin.sh --all or remove the --all flag and specific the specific vault you want.
| Change | Rebuild? | Why |
|---|---|---|
main.ts or src/*.ts | Yes | Must compile to main.js before Obsidian loads it. |
styles.css | No | Loaded at startup. Reload Obsidian only. |
manifest.json | No | Loaded once at startup. Reload Obsidian only. |
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.