# dsh-obsidian

Connect [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) to a local [Obsidian](https://obsidian.md) vault. Because an Obsidian vault is just a folder of Markdown files on disk, your `dsh` agent can search, read, write, move, and trash notes directly — **no MCP server, no OAuth**.

[![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Node: >=22.12.0](https://img.shields.io/badge/Node-%3E%3D22.12.0-339933.svg)](https://nodejs.org) [![dsh: v0.1.5-alpha.1 verified](https://img.shields.io/badge/dsh-v0.1.5--alpha.1%20verified-2ea44f.svg)](https://github.com/deepseek-ai/deepseek-harness)

[中文](README.md) | English

## What it does

Once `dsh-obsidian` is installed, your `dsh` agent can read and write a local Obsidian vault directly. On startup the plugin detects your vault (or reads the path you configured) and mounts 12 `obsidian_*` tools covering search, read, write, append, move, delete, and backlink lookups.

## Features

- **Zero server** — reads/writes the vault filesystem directly; no Local REST API community plugin, no standing MCP server.
- **Safe by default** — deletes move notes into `.trash/` (reversible), paths can't escape the vault root, and `.obsidian/` is never touched.

## How it works

```text
dsh agent calls obsidian_* tools
   │
   ▼
VaultAccess interface
   └─ FsAccess — node:fs + hand-written frontmatter/wikilink parsing (default, pure filesystem)
```

On startup the plugin resolves the vault root as "explicit `vaultPath` wins, else auto-detect from `obsidian.json`"; when `useCli` is on and the CLI is detected, `property:set`/`property:remove` delegate to it and everything else stays on `FsAccess`; any CLI failure silently falls back to `FsAccess`.

## Install

```sh
dsh plugin --profile web add dsh-obsidian
```

Replace `web` with the profile you run your agent in (`web`, `headless`, `tui`, …).

## Update

Re-running `add` pulls the latest (`latest`):

```sh
dsh plugin --profile web add dsh-obsidian
```

Or pin a specific version:

```sh
dsh plugin --profile web add dsh-obsidian@0.2.5
```

Restart the harness (`dsh web`) or refresh the Web UI after updating; verify with `dsh plugin --profile web list`.

## Uninstall

```sh
dsh plugin --profile web remove dsh-obsidian
```

## Configuration

| Key | Default | Description |
| --- | --- | --- |
| `vaultPath` | (auto-detected) | Absolute path to the vault; leave empty to auto-detect the currently-open vault from `obsidian.json` |
| `useCli` | `false` | Delegate `property:set`/`property:remove` to the `obsidian` CLI when available |
| `excludeDirs` | `[".obsidian", ".git", ".trash"]` | Directories excluded from search and list |

### Paths and Windows compatibility

- Tool paths are vault-relative and are always returned with `/`; inputs may use either `/` or `\\`.
- Tool path arguments must be relative. Unix absolute paths, Windows drive-letter paths, and UNC absolute paths are rejected; the configured `vaultPath` may be a local Windows path or a UNC vault root.
- Windows note identity is case-insensitive and recognizes `.md`, `.MD`, and other case variants; Unix keeps case-sensitive behavior and the existing `.md` semantics.
- Search removes `\\r` from CRLF lines while preserving Unicode paths and content. `ripgrep` is an optional accelerator: if it is unavailable or fails, the built-in scanner is used automatically.
- Vault discovery prefers the platform-native `obsidian.json` location (Windows `%APPDATA%`, macOS `Library/Application Support`, Linux `.config`) before trying other known locations. `.exe`/`.cmd` CLI forms are supported and filesystem access remains the fallback.

## Tools

| Tool | Purpose |
| --- | --- |
| `obsidian_list` | List notes in the vault (filter by subdirectory, limit results) |
| `obsidian_search` | Full-text search with match lines + context (case-insensitive; Windows recognizes mixed-case `.md` extensions) |
| `obsidian_read` | Read a note (body + parsed frontmatter) |
| `obsidian_frontmatter` | Read only a note's YAML properties |
| `obsidian_backlinks` | Find notes linking to a note via `[[wikilinks]]` |
| `obsidian_write` | Create or overwrite a note (creates parent directories) |
| `obsidian_append` | Append text to the end of a note |
| `obsidian_move` | Move/rename a note (updates `[[wikilinks]]` in pure filesystem) |
| `obsidian_delete` | Trash a note into `.trash/` (reversible, never permanently deletes) |
| `obsidian_set_property` | Set or update a single frontmatter property (YAML) on a note |
| `obsidian_delete_property` | Remove a frontmatter property from a note |
| `obsidian_tags` | List all tags in the vault with usage counts |

All tool path arguments are relative to the vault root (e.g. `Folder/note.md`); paths returned to the agent always use `/`.

## Safety

- **Path containment** — every path argument is resolved and checked to stay inside the vault root; escapes (`../`, Unix/Windows absolute paths, or UNC paths) are rejected.
- **Reversible delete** — `obsidian_delete` only moves notes into the vault's `.trash/`, never permanently deletes.
- **Hands off `.obsidian/`** — search and list exclude `.obsidian/`, `.git/`, and `.trash/` by default.
- **Preserves frontmatter and wikilinks** — reads/writes don't break YAML properties or `[[links]]` (unless the task explicitly asks).

## Requirements

- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`)
- Node.js ≥ 22.12.0

Compatibility has been verified through `dsh` v0.1.5-alpha.1 (including v0.1.3-alpha.2, v0.1.3-alpha.1, v0.1.2-rc.1, v0.1.2-alpha.5, v0.1.2-alpha.4, v0.1.2-alpha.3, v0.1.2-alpha.2, v0.1.2-alpha.1, v0.1.1-rc.2, and v0.1.0-rc.8).

## Development

```sh
npm install
npm run build      # tsdown → lib/
npm run typecheck  # tsc --noEmit
npm test           # vitest
```

## Changelog

### 0.2.5

- Fix double quoting of Windows `.cmd` scripts so commands work when script paths or arguments contain spaces or Unicode characters.

### 0.2.4

- Harden Windows compatibility: standardize agent-facing paths on `/`, accept `\\` inputs, and reject drive-letter/UNC absolute path escapes.
- Fix Windows search, backlink, move/delete result paths, and wikilink rewriting; cover CRLF, Unicode, and mixed-case Markdown extensions such as `.MD`.
- Support Windows `.exe`/`.cmd` forms for `ripgrep` and the Obsidian CLI, with automatic fallback to built-in implementations when unavailable or failing.

### 0.2.3

- Fix source typecheck/build compatibility after `dsh` v0.1.2-alpha.2/alpha.3 stopped re-exporting `JsonValue` from `@deepseek-ai/dsh-tools`, while preserving compatibility with older dsh releases.

### 0.2.2

- `obsidian_tags` and inline `#tag` extraction now support CJK and other Unicode characters.
- `obsidian_backlinks` / `obsidian_move` resolve `[[wikilinks]]` uniquely following Obsidian's rules: same-named notes are no longer over-matched, and ambiguous links are no longer mis-rewritten.
- `obsidian_set_property` / `obsidian_delete_property` preserve existing YAML comments, anchors/aliases, and block formatting instead of re-serializing.
- Reliability: atomic writes when `obsidian_move` updates links, fallback for cross-filesystem moves/deletes, and consistent search results with or without ripgrep.

## License

[MIT](LICENSE) © 2026 MingZeng
