› cavi-ai/
GitHub ↗

Architecture

A map of the Obsidian plugin for anyone reading or contributing to the code. Everything below lives under obsidian-plugin/src/.

main.ts is the Obsidian Plugin subclass. It registers the chat view, the claude-html Markdown code-block processor, commands, the settings tab, and starts the MCP bridge on layout-ready. Everything else is organized by concern.

Module map

ModuleResponsibility
providers/ProviderRouter builds providers from settings and routes chat among Anthropic, Claude Code CLI, Ollama, auto fallback, and OpenAI-compatible endpoints while routing utility work independently. chatTurn.ts settles one streaming attempt as a turn result and routes a chat turn: a partial agent answer is kept with a notice, the on-device backend never falls back, and other failures retry locally when the fallback policy allows. anthropic.ts/claudeCli.ts/ollama.ts/openaiCompat.ts implement the provider boundary; ollamaBody.ts/ollamaParse.ts are the pure Ollama wire format; jsonRepair.ts is the shared complete→parse→repair loop; auth.ts resolves direct-API credentials; fallback.ts owns offline/usage fallback; endpointPolicy.ts classifies local endpoints; utilityConsent.ts holds the session's mobile fallback consent: one decision per exact source and destination, one shared dialog, a denial no racing allow replaces, and deny after unload; utilityRouting.ts resolves the runtime utility selection, asks that consent through the mobile dialog, and owns the unload lifecycle that discards in-flight utility work; errorHints.ts preserves causes and adds actionable guidance.
claude/sse.ts parses the Anthropic streaming response; models.ts resolves the model id (dropdown vs custom).
cli/The desktop Claude Code chat runtime: executable/auth probing, JSONL stream parsing, per-conversation resume state, abort/restart handling, and a per-session ephemeral tool bridge that keeps approvals and Plan Mode scoped to the originating conversation. sessionPool.ts keeps one session per conversation, reuses it while its signature holds and the process is alive, evicts the least recently used idle session at capacity, runs calls for one conversation in order so a close waits for a session still opening, and releases each session's bridge and prompt file after closing it.
web/search.ts is the agent's web search (DuckDuckGo HTML and Brave adapters behind one result shape); fetch.ts reads one public page as bounded readable markdown. Pure, IO injected.
artifacts/designSystem.ts holds the design-system prompt injected into every system prompt; parse.ts extracts claude-html blocks; renderInline.ts renders them in a sandboxed iframe with a restrictive CSP; artifactStore.ts saves them as notes.
context/vaultContext.ts assembles per-request context (active note, selection, linked/backlinked notes, vault search, attached pages) within contextCharBudget; search.ts is keyword scoring; attachments.ts handles vault PDFs/images and pasted screenshots as multimodal blocks; urlContext.ts detects a pasted URL and shapes the attached-page record, with webCapture.ts doing the Defuddle fetch on explicit user action only.
templates/promptTemplates.tsUser-defined prompt templates: parse a template note's frontmatter (name, description, optional model and context defaults) plus body into a slash command, and substitute {selection}/{active_note} at run time. Pure — vault IO lives upstream.
semantic/The local embeddings index: indexer.ts orchestrates traverse → chunk → embed → store (IO injected, markdown and PDF); pdf.ts extracts and page-locator-chunks vault PDFs (pdf.js loader injected; pdfjs.ts wires the bundled worker); store.ts is a pure vector store with (de)serialization; similarity.ts is vector math plus reciprocal-rank fusion; recovery.ts classifies an embedding failure (missing built-in model, unreachable Ollama or custom endpoint, missing model, mobile loopback endpoint, storage failure) and names the action that fixes it.
mcp/server.ts is the token-gated loopback HTTP server with negotiated sessions and authenticated deletion; protocol.ts implements MCP 2025-06-18-compatible JSON-RPC, tools, resources, and prompts; providers.ts exposes vault-note resources and workflow/template prompts; toolRecord.ts declares each tool once (definition, write flag advertised as readOnlyHint, listing condition, handler); vaultTools.ts assembles its listing, write gate, and dispatch once per options from the record groups in tools/ (search, notes, structure, web, memory, plus the research tools), keeping reads always available and mutations behind mcpAllowWrites; edit.ts powers targeted note_patch; clientConfig.ts emits connection snippets. The MCP client uses client.ts, external.ts, httpTransport.ts/stdioTransport.ts, and externalManager.ts for external servers in agent mode.
agent/loop.ts runs the stream → execute tools → re-stream turn (pure, deps injected); toolAccess.ts decides, per run kind (off, Plan Mode, chat, propose-only), which tools are offered and whether each call runs, needs confirmation, goes to edit review, or is refused; tools.ts adapts MCP tool defs to Anthropic tool-use, executes calls by that decision, and truncates results; prompt.ts holds the agent system-prompt addendum and the Plan Mode instruction.
edit/ and editor/edit/diff.ts validates exact replacements and applies accepted hunks; edit/rewrite.ts builds the rewrite and insert-at-cursor prompts. editor/inlinePrompt* is the instruction prompt typed at the cursor and editor/inlineEdit.ts runs it to review. editor/inlineDiff* renders word-level review inside CodeMirror, while editor/reviewEdits.ts selects the inline path for an open note and falls back to the modal.
links/unlinkedMentions.ts finds plain-text occurrences of other notes' titles/aliases; suggest.ts merges them with semantic neighbors into one ranked list; batch.ts plans link edits across many notes and inboxBatchReview.ts turns those plans into one reviewable batch. Pure.
build/The spec→build handoff. spec.ts extracts tasks from a plan note and renders the build spec plus Claude Code command; tracker.ts renders the live progress board as a claude-html artifact.
cloud/The mobile-friendly cloud loop: routines.ts dispatches a Claude Code cloud session; replies.ts pulls reply notes from the vault's GitHub repo over the Contents API; setup.ts derives the per-step setup state behind the settings checklist. Pure.
publish/Publishing notes and artifacts as GitHub Gists: controller.ts orchestrates publish, republish and unpublish with injected IO, gist.ts builds requests, noteTransform.ts and artifactPage.ts produce the published content, registry.ts is the persisted item list; publishFeature.ts owns the controller, the registry and the cloud controller (plugin.publishing()).
workflows/ and skills/catalog.ts holds Companion-native workflow adaptations; skillRegistry.generated.ts is built from the pinned obsidian-agent capabilities and skill sources; skills/compose.ts turns an invoked portable skill into a grounded chat turn.
indexing/frontmatter.ts builds YAML frontmatter on every saved artifact/chat so it indexes in search, the tag pane, and Dataview; autoTagger.ts suggests tags reusing existing vault tags.
memory/Session memory: sessions.ts + transcript.ts discover and digest Claude Code CLI sessions (the Node fs reader is desktop-only and lazy-imported); consolidate.ts merges digests into the evolving "What Claude Knows" note.
research/The Research Desk: projects, sources, evidence, claims, questions, documents as typed Markdown notes. repository.ts is the vault-backed store; parse.ts/render.ts round-trip records; identity.ts dedups sources; graph.ts builds snapshots; audit.ts flags problems; draft*/revision* implement claim-grounded drafting and claim-preserving revision; webCapture.ts captures web sources as clean markdown; tools.ts defines the research_* MCP tools; researchFeature.ts builds the Desk view and owns discovery coordinators, refresh scheduling and the plan-to-build handoff (plugin.research()).
discovery/Scholarly discovery: adapters/ (OpenAlex, Crossref, arXiv, Zotero item lookup), coordinator.ts orchestrates search → merge → rank → optional model rerank → import; safeUrl.ts scheme-checks URLs.
sources/Typed source capture: watcher.ts decides which new inbox files to enrich, detect.ts classifies the type, registry.ts holds per-type frontmatter schemas, enrich.ts fills them (constrained-JSON utility calls, thinking disabled), frontmatterMerge.ts unions tags so clips keep their own, enrichmentQuality.ts rejects placeholder titles, lost metadata, and malformed provenance inside the atomic write, clipperSetup.ts derives the template install state and clipperVerification.ts checks clipped notes against the expected type, schema version, and destination, clipperFeature.ts runs template export, the setup modal, and arriving-clip verification, organize.ts proposes domain folders and plans collision-safe rename+moves for the clipping organizer.
onboarding/firstRun.ts and wizard.ts are the pure ordering of the one-time consent prompts and the setup wizard steps; onboarding.ts runs them: layout-ready first run, the single-instance wizard, the ontology seed and reload.
ontology/Vault ontology: schema.ts parses schema notes into resolved types with inheritance; describe.ts shapes tool-readable registry output; propose.ts validates proposed schema notes; conform.ts performs advisory checks and safe auto-fixes; digest.ts injects a compact type digest into the system prompt.
canvas/jsonCanvas.tsValidates a model-proposed node/edge graph into JSON Canvas 1.0 with deterministic auto-layout.
bases/baseFile.tsValidates a model-proposed database view and emits Obsidian Bases .base YAML.
conversations/store.tsPure conversation store — saved chats survive restarts and resume from a session list.
usage/tokens.tsToken counting and cost estimation for the context gauge and session totals.
commands/definitions.tsThe command-palette surface as data — ids, names, and availability rules behind a CommandActions interface, so main.ts keeps only the wiring and the set is testable without an Obsidian app; the chat's plugin-level slash actions dispatch through the same interface (view/slashCommands.ts).
composition/Wiring moved out of main.ts: buildCommandActions (palette and slash actions), vaultToolOptions (MCP bridge and in-chat agent tools), buildHealthController / buildSystemDeps (System page), buildChat (conversations, distiller, session actions, turn service), buildSemantic / buildEnrichment / buildMemory / buildClippings (semantic index, source enrichment, session memory, clippings), buildBuildController / buildMcpBridge / buildOptimize (build, MCP bridge, optimize), and buildUtilityRouting / buildClipperSetup / buildOnboarding / buildNoteFlows / buildWorkspaceViews / buildShell / buildResearch / buildDesktopIntegrations / buildOrders / buildPublishing (the feature objects). Each takes a Pick of the plugin members it uses.
chat/turnService.ts owns in-flight turns independent of any chat view; turnStart.ts decides whether a send may start (credential and CLI sign-in preflight), prepares the turn's messages, begins it durably, leaving the view's messages untouched when the begin fails, and builds the persistence hooks the turn service calls; turnLifecycle.ts holds live cancellation handles; turnNotice.ts decides the unattended-completion notice.
view/The side-panel chat UI plus the Research Desk view, diff modal, pickers, and slash/at menus. chat/chatMode.ts owns a chat's Ask / Plan / Act (the persisted writes setting with rollback on a failed save, the chat's Plan Mode, tool capability, and the turn's tool access); the composer switch, the desktop ⋯ menu, and Implement all read it. chat/keyboardPin.ts keeps a focused chat input above the mobile keyboard by undoing the page, visual viewport, and clipped-ancestor scroll WebKit adds when revealing it. chat/stickToBottom.ts keeps the transcript on its latest message while the reader is at the bottom, through new or re-rendered content and a shrinking viewport, and stops following once they scroll up. chat/chatSession.ts holds the conversation state the composer, header, and transcript share (messages, streaming, controls, project, write grant, turn and usage, mode); the composer exposes only its draft and reasoning indicator to the others. chat/hosts.ts names the plugin capabilities each chat module and the chat view may use; the Inbox, memory, system, and related views, the settings tab, and its page builders declare theirs the same way, so no module takes the whole plugin. BatchDiffModal.ts reviews many notes at once over the pure batchDiffState.ts selection model; inboxRefresh.ts coalesces Inbox rescans and drops superseded ones; quickOptions.ts derives each page's focused option menu (settings last) and QuickOptionsModal.ts renders it; ActivityIndicator.ts renders the activity drawer over the shared store, ordering needs-attention work first; companionChrome.ts mounts both on every Companion view; ClipperSetupModal.ts walks through installing the generated clipper templates; workspaceViews.ts opens and reuses the Companion leaves and runs the Inbox badge; companionShell.ts owns quick options, settings openers, and activity recovery.

The pure-module pattern

Obsidian-free logic — SSE parsing, artifact extraction, search scoring, frontmatter, MCP protocol framing, provider parsing, the agent loop, diff application, fallback policy — is factored into pure modules with no Obsidian imports, so it unit-tests without a running app.

Modules that require obsidian are tested against an in-memory fake: vitest.config.ts aliases the obsidian module to test/fakes/obsidian.ts.

The rule for new code: keep the logic in a pure module and test it directly. Injection is used consistently to make that possible — loop.ts takes its stream and tool executor as deps, indexer.ts takes its IO, consolidate.ts takes file contents and the model call.

More than 2,100 tests across 223 Vitest files cover the pure and Obsidian-backed modules, grouped by concern (test/research/, test/semantic/, test/discovery/, test/ontology/, test/sources/, test/mcp/). CI runs version lockstep, typecheck, lint, test, the docs-artifact and generated-skill contracts, build, and a production bundle budget on Node 20 and 22, plus a production dependency audit and real-Obsidian Playwright suite on 22.

bash
pnpm run typecheck   # tsc --noEmit --skipLibCheck
pnpm run lint        # eslint src (tests are typechecked, not linted)
pnpm test            # vitest run
pnpm run build       # typecheck + production bundle
pnpm run bundle:check # enforce the production main.js byte budget
pnpm run test:mcp-eval # mcp-eval readiness of the MCP bridge in real Obsidian, against e2e/mcp-eval.baseline.json

Bundling

esbuild.config.mjs bundles src/main.ts → main.js (CJS, es2021) in three passes:

  1. Bundle src/semantic/transformers/worker.ts — transformers.js included — to .build/embed-worker.txt.
  2. Bundle the pdf.js worker (PDF text extraction) to .build/pdf-worker.txt.
  3. Inline both files as text (loader: { ".txt": "text" }) into main.js, which instantiates them via Blob-URL workers at runtime.

obsidian, electron, the CodeMirror packages, and all Node builtins are marked external. The MCP server uses Node http, which exists at runtime on desktop because Obsidian's Electron exposes Node — and is reached through a guarded dynamic import with inline import("http") types, so the bundle never statically imports a Node builtin and stays loadable on mobile.

Desktop-only features are the Claude Code chat backend, MCP bridge, Ollama, stdio MCP clients, and session import, all gated at runtime. Direct-API chat, artifacts, and built-in semantic search run on mobile.

Security model

Four boundaries, each documented in `SECURITY.md`:

Credentials live in Obsidian plugin settings on the user's device. Never logged. Password-typed inputs. The MCP token can be sourced from OBSIDIAN_COMPANION_MCP_TOKEN instead, to keep it out of a synced vault.

The MCP bridge must bind 127.0.0.1, require a non-empty bearer token (startup fails without one), compare it in constant time, reject non-loopback Host headers, and keep write tools disabled unless explicitly enabled. Don't widen the bind address, allow a tokenless mode, or expose writes by default.

Artifacts are model-generated HTML and must stay sandboxed without allow-same-origin, with network and form submission blocked by CSP. See artifacts.md for the exact policy.

Auth surface separates the installed Claude Code CLI backend—which owns its own sign-in—from direct API authentication using a user-provided key, a long-term token from claude setup-token, or environment import. Browser/session cookies from claude.ai are not accepted credentials.

Repo layout

One standalone Obsidian product plus an optional universal-agent reference:

  • obsidian-plugin/ — Companion for Claude, the Obsidian community plugin.
  • claude-plugin/ — compatibility-named pinned submodule of

`obsidian-agent`, whose portable workflows use the official Obsidian CLI and do not depend on Companion or MCP. Companion-native adaptations live in this repository and use Companion's own vault context. The optional MCP bridge is a separate Companion integration.

  • upstream/html-effectiveness/, upstream/obsidian-skills/ — pinned, unmodified submodules. Provenance in `NOTICE`.

Version numbers in manifest.json, versions.json, and package.json must match the git tag, and versions.json maps version → minAppVersion. See `CONTRIBUTING.md`.