Skip to content
dsh.fish
Bundle

dsh-obsidian-bridge

Bidirectional knowledge bridge between DeepSeek Harness and Obsidian Vault — FTS5 search, draft writing, session linking

Source
AIMarshallLee
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-obsidian-bridge

Bidirectional knowledge bridge between [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) and [Obsidian](https://obsidian.md) Vault.

## What It Does

Gives DSH agents direct read/write access to your Obsidian Vault. The agent can search notes via SQLite FTS5 full-text search, read full content by path or wiki-link, write draft notes back to the vault, and create bidirectional links between DSH sessions and notes.

Unlike existing one-way export tools or MCP-based bridges, this plugin runs natively inside the DSH Cordis framework — no external server, no need for Obsidian to be running.

## Status: v0.2.0 — Open Source Alpha

### Verified Capabilities

| Capability | Status | Test |
|-----------|--------|------|
| Read note by file path | Pass | `test/spike.mjs` |
| Read note by wiki-link name | Pass | `test/spike.mjs` |
| Read note with spaces in name | Pass | `test/spike.mjs` |
| SQLite FTS5 full-text search | Pass | `test/phase2-spike.mjs` |
| BM25 relevance ranking | Pass | `test/phase2-spike.mjs` |
| Chinese text search (CJK tokenization) | Pass | `test/phase2-spike.mjs` |
| Incremental sync (mtime-based) | Pass | `test/phase2-spike.mjs` |
| 500-note search performance | Pass (34ms) | `test/spike.mjs` |
| Path traversal protection | Pass | `test/spike.mjs` |
| Plugin loads in Cordis context | Pass | `test/integration.mjs` |
| Tool registration via defineTool | Pass | `test/integration.mjs` |
| Tool execute + render pipeline | Pass | `test/integration.mjs` |
| System prompt section injection | Pass | `test/integration.mjs` |
| Write draft note to vault | Pass | `test/phase1-spike.mjs` |
| Write draft with Unicode title | Pass | `test/phase1-spike.mjs` |
| Link session to vault note | Pass | `test/phase1-spike.mjs` |
| Link multiple notes to same session | Pass | `test/phase1-spike.mjs` |
| Keyword extraction (EN + CN) | Pass | `test/phase1-spike.mjs` |
| Auto-context hook (agent/pre-step) | Pass | `test/phase1-integration.mjs` |
| Auto-capture hook (tools/result) | Pass | `test/phase1-integration.mjs` |
| Auto-capture skips obsidian_* tools | Pass | `test/phase1-integration.mjs` |
| Zod config schema validation | Pass | `test/phase2-spike.mjs` |
| Plugin config via cordis.patch.yml | Pass | `test/phase2-integration.mjs` |
| Env var fallback compatibility | Pass | `test/phase2-integration.mjs` |
| Plugin disabled without vault path | Pass | `test/phase2-integration.mjs` |
| Hook disposer cleanup | Pass | `test/phase2-integration.mjs` |
| Plugin lifecycle cleanup (ctx.effect) | Pass | `test/phase2-integration.mjs` |

### Test Results

```
Phase 0 Spike:        25 passed, 0 failed
Phase 0 Integration:  49 passed, 0 failed
Phase 1 Spike:        35 passed, 0 failed
Phase 1 Integration:  45 passed, 0 failed
Phase 2 Spike:        35 passed, 0 failed
Phase 2 Integration:  23 passed, 0 failed
Total:               212 passed, 0 failed
```

## Architecture

```
DSH Agent (Node.js process)
  └─ Cordis Framework
       └─ dsh-obsidian-bridge plugin (apply(ctx, config))
            ├─ Config Schema (zod: vaultPath, autoContext, autoCapture, draftFolder, sessionId)
            ├─ System Prompt: "obsidian-vault-aware" (tools + workflow guide)
            ├─ Tool: obsidian_search (FTS5 BM25 search across vault notes)
            ├─ Tool: obsidian_read_note (read full note by path or wiki-link)
            ├─ Tool: obsidian_write_draft (write draft to dsh-drafts/ folder)
            ├─ Tool: obsidian_link_session (bidirectional session-note linking)
            ├─ Hook: agent/pre-step (auto-inject vault context, opt-in)
            ├─ Hook: tools/result (auto-capture tool results, opt-in)
            ├─ ctx.effect() cleanup (closes SQLite, disposes tools/hooks)
            └─ VaultManager (file system access, caching, wiki-link resolution)
                 ├─ NoteIndex (SQLite FTS5 full-text index)
                 │    ├─ Incremental sync (mtime-based, throttled)
                 │    ├─ BM25 relevance ranking
                 │    ├─ Chinese CJK tokenization
                 │    └─ .dsh/index.db (WAL mode, 16MB cache)
                 └─ Obsidian Vault (plain Markdown files on disk)
```

Single process. No external server. Direct file system access.

## Installation

### Prerequisites

- Node.js >= 22.19 (or >= 24)
- DSH installed: `npm install -g @deepseek-ai/dsh`
- An Obsidian Vault directory

### Build

```bash
cd dsh-obsidian-bridge
npm install
npm run build
```

### Install into DSH

**Option A: From GitHub (recommended — no npm account needed)**

```bash
# Install directly from GitHub
dsh plugin --profile web add github:Daseanle/dsh-obsidian-bridge

# Configure via cordis.patch.yml
dsh web
```

**Option B: From npm (when published)**

```bash
dsh plugin --profile web add dsh-obsidian-bridge
dsh web
```

**Option C: From local clone**

```bash
git clone https://github.com/Daseanle/dsh-obsidian-bridge.git
cd dsh-obsidian-bridge && npm install && npm run build
dsh plugin --profile web add file:./dsh-obsidian-bridge
dsh web
```

### Configuration

Create or edit `cordis.patch.yml` in your DSH config directory:

```yaml
- id: dsh-obsidian-bridge
  config:
    vaultPath: /path/to/your/vault
    autoContext: false        # auto-inject vault context before each model step
    autoCapture: false        # auto-capture non-Obsidian tool results as drafts
    draftFolder: dsh-drafts   # folder for draft notes
    sessionId: ""             # optional: link drafts to a specific session
```

Or use environment variables (legacy, used as fallback when config values are not provided):

```bash
export OBSIDIAN_VAULT_PATH="/path/to/your/vault"
export DSH_OBSIDIAN_AUTO_CONTEXT=true   # optional
export DSH_OBSIDIAN_AUTO_CAPTURE=true   # optional
export DSH_SESSION_ID="my-session"      # optional
```

### Verify

In the DSH agent, ask it to search your vault:
```
Search my Obsidian vault for notes about "architecture"
```

## Project Structure

```
dsh-obsidian-bridge/
├── src/
│   ├── index.ts              # Plugin entry: apply(ctx, config) with zod schema
│   ├── vault-manager.ts      # Core: path resolution, note reading, search, write, link
│   ├── note-index.ts         # SQLite FTS5 full-text index with incremental sync
│   ├── hooks.ts              # Event hooks: auto-context, auto-capture (returns disposer)
│   └── tools/
│       ├── read-note.ts      # obsidian_read_note tool definition
│       ├── search.ts          # obsidian_search tool definition
│       ├── write-draft.ts    # obsidian_write_draft tool definition
│       └── link-session.ts   # obsidian_link_session tool definition
├── dist/                      # Compiled JavaScript (tsc output)
├── test/
│   ├── vault/                 # Test Obsidian Vault (4 sample notes)
│   ├── spike.mjs              # Phase 0 core logic tests (25 tests)
│   ├── integration.mjs        # Phase 0 plugin lifecycle tests (49 tests)
│   ├── phase1-spike.mjs       # Phase 1 write/link tests (35 tests)
│   ├── phase1-integration.mjs # Phase 1 tools+hooks tests (45 tests)
│   ├── phase2-spike.mjs       # Phase 2 FTS5+config tests (35 tests)
│   └── phase2-integration.mjs # Phase 2 config+cleanup tests (23 tests)
├── cordis.patch.yml          # Example plugin configuration
├── package.json
├── tsconfig.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
└── .npmignore
```

## Configuration

### Config Schema (zod)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `vaultPath` | string | `""` | Absolute path to your Obsidian Vault |
| `autoContext` | boolean | `false` | Auto-inject vault context before each model step |
| `autoCapture` | boolean | `false` | Auto-capture non-Obsidian tool results as drafts |
| `draftFolder` | string | `"dsh-drafts"` | Folder name for draft notes |
| `sessionId` | string | `""` | Session ID for draft frontmatter and session linking |

Config is validated via zod Standard Schema V1. Values can be provided through `cordis.patch.yml` or environment variables (as fallback).

### Environment Variables

| Variable | Fallback For | Default |
|----------|-------------|---------|
| `OBSIDIAN_VAULT_PATH` | `vaultPath` | — |
| `DSH_OBSIDIAN_AUTO_CONTEXT` | `autoContext` | `false` |
| `DSH_OBSIDIAN_AUTO_CAPTURE` | `autoCapture` | `false` |
| `DSH_SESSION_ID` | `sessionId` | `""` |

### SQLite FTS5 Index

The plugin creates a `.dsh/index.db` SQLite database inside your vault directory:

- **FTS5 virtual table** with `unicode61` tokenizer
- **UNINDEXED columns** for metadata (path, frontmatter, mtime, etc.)
- **BM25 relevance ranking** for search results
- **Incremental sync**: only re-reads notes whose `mtime` changed
- **Throttled sync**: skips re-sync within 2s of last sync (unless invalidated)
- **Chinese support**: CJK characters are split for unicode61 tokenization
- **WAL mode** with 16MB cache for performance

## Roadmap

| Phase | Scope | Status |
|-------|-------|--------|
| Phase 0 — Spike | read_note + search, in-memory index, path traversal guard | Complete |
| Phase 1 — Self-use MVP | write_draft + link_session tools, hooks (auto-context, auto-capture) | Complete |
| Phase 2 — Open Source Alpha | SQLite FTS5, cordis.patch.yml config, npm publish, documentation | Complete |

## License

MIT

Install

dsh plugin --profile web add github:AIMarshallLee/dsh-obsidian-bridge

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source