# @bacnh85/pi-obsidian

Pi extension for **Obsidian vault tools** — a single unified tool that runs any Obsidian CLI command: read, write, create, search, delete, move, rename, append, prepend, tasks, properties, history, daily notes, templates, and more. Plus **enhanced operations** like recursive file listing, task creation, task filtering/grouping, and template-based note creation.

**~6,500 fewer tokens per request** vs 43 separate tools (1.6K vs 8.1K).

## Fixes & Improvements

| Area | Detail |
|------|--------|
| **Write reliability** | Write operations (create/write/append/prepend) route through base64-encoded `eval` — no escaping issues with `\`, `"`, `|`, literal `\n`, or large payloads. `content_from` reads from a vault note for convenience. |
| **Properties** | `property:set` auto-normalizes spaces in array values; rejoins split tokens. |
| **Directory handling** | `ensureFolder()` creates parent path before writing. Handles root folder `/` and empty folder string. |
| **Tag operations** | `tag-rename from=X to=Y` command scans all files and renames tags across the vault. |
| **Script execution** | `eval file=ScriptNoteName` reads JS from a vault note; bare expressions are auto-wrapped with `return`, and the eval body runs in a try/catch (thrown errors come back as `Error: ...` output). |

## Requirements

- **Obsidian 1.12+** with CLI enabled in **Settings → General → Command line interface**
- **Obsidian desktop app must be running** — the CLI communicates via IPC
- The `obsidian` binary must be in your PATH (the installer handles this)

## Install

```bash
pi install npm:@bacnh85/pi-obsidian
```

## Configuration

**Zero env vars.** Just make sure `obsidian` is in your PATH. Vault targeting uses `vault=<name>` as a parameter when needed; the CLI defaults to the most recently focused vault.

## Vault protection

When Pi runs from a directory inside an Obsidian vault (a `.obsidian/` directory is found in the current directory or an ancestor), the extension blocks generic `read`, `write`, `edit`, `ls`, `find`, `grep`, and direct filesystem `bash` operations that target that vault. Use `obsidian` for vault files instead. If that vault is not the currently focused Obsidian vault, pass its explicit `vault=<name>` to prevent an operation from targeting the wrong vault. Normal shell commands and explicit paths outside the vault remain available.

## Usage

One tool: `obsidian` with a `run` parameter containing the full CLI command.

```
obsidian run="read file=Meeting Notes" vault="My Vault"
```

### Standard CLI commands

| Category | Example |
|----------|---------|
| **Read** | `read file="Meeting Notes"` |
| **Create** | `create path=folder/note.md overwrite=true content="# Title\n\nBody"` | Content is base64-encoded via `eval` — no size limit or escaping issues |
| **Create (from note)** | `create path=note.md content_from=SourceNoteName` | Reads content from an existing vault note (bypasses CLI escaping); same chunking applies |
| **Append** | `append path=note.md content="More text"` |
| **Prepend** | `prepend path=note.md content="# Header"` |
| **Delete** | `delete path=old.md permanent=true` |
| **Move** | `move file=Note to="01 Projects/"` |
| **Rename** | `rename file=Note name="New Name"` |
| **Search** | `search query=roadmap limit=10` |
| **Search grouped** | `search query=roadmap group=file` | Results grouped by file |
| **Tags** | `tags counts=true sort=count format=json` |
| **Tag** | `tag name="#type/reference" verbose` |
| **Tag rename** | `tag-rename from="#moc" to="#type/moc"` | Renames a tag across all files |
| **Eval (inline)** | `eval code="app.vault.getFiles().length"` | Run JS inline |
| **Eval (file)** | `eval file=ScriptNoteName` | Run JS from a vault note (avoids escaping issues) |
| **Properties** | `property:set file=Note name=status value=active` |
| **Daily note** | `daily:read`, `daily:append content="- [ ] Task"` | Note: `daily:read`/`append`/`prepend` are CLI-native. `daily:today`/`daily:open` require the Obsidian desktop app and are not available via CLI.
| **Backlinks** | `backlinks file=Note format=json` |
| **Outline** | `outline file=Note` |
| **History** | `history file=Note`, `diff file=Note from=1 to=3` |
| **Vault info** | `vault` |
| **Files** | `files folder="01 Projects"` | Direct children |
| **Files (root)** | `files folder="/"` | Root-level files |
| **Files (recursive)** | `files folder="01 Projects" recursive` | Recursive listing |

### Enhanced commands

These are post-processed by the extension for richer output:

| Command | Example | What it does |
|---------|---------|-------------|
| **Recursive files** | `files folder="01 Projects" recursive` | Recursively list all files under a folder by traversing subfolders |
| **Tasks grouped** | `tasks format=json group=file` | Lists all tasks grouped by source file |
| **Tasks filtered** | `tasks format=json status=open` | Lists only open (`[ ]`) or done (`[x]`) tasks |
| **Tasks filtered+grouped** | `tasks format=json group=file status=done` | Done tasks grouped by file |
| **Search grouped** | `search query=roadmap group=file` | Search results grouped by file with line numbers |
| **Task creation** | `task-create path=note.md heading="Tasks" text="Buy milk"` | Adds a task line under the specified heading; creates the heading if missing |
| **Create from template** | `create-from-template template="Project Brief" name="My Project" folder="01 Projects" title="..."` | Reads a template, fills `{{placeholder}}` values, writes a new note |
| **Files by missing property** | `files missing-property=created` | Lists markdown notes whose frontmatter lacks the given property |
| **Frontmatter tag validation** | `files validate-tags="type/,domain/"` | Reports notes with missing/invalid tags (defaults to `type/,domain/` dimensions) |
| **Property rename** | `property:rename from=date to=created` | Renames a frontmatter property across all files |
| **Search with replace** | `search query=old replace=new regex=true preview=true` | Replace across files; `preview=true` for dry-run, omit to apply |
| **Frontmatter wrap** | `frontmatter:wrap` | Wraps leading `title:`/`tags:` lines of notes lacking frontmatter into a `---` block |

### Syntax rules

- **Quote values with spaces:** `file="My Note"`, `query="search phrase"`
- **Boolean flags:** `permanent`, `overwrite`, `total`, `silent`, `inline`, `verbose`, `recursive`
- **For JSON output:** add `format=json` flag
- **Target a vault:** add `vault="Vault Name"` to any command
- **Multiline content:** use `\n` for newlines, `\t` for tabs

## How it works

Previously this extension registered 43 separate tools (`obsidian_read`, `obsidian_create`, `obsidian_search`, etc.), consuming ~8,122 tokens per request. v0.3 consolidated to a single `obsidian` tool that parses the `run` string and dispatches to the Obsidian CLI. v0.4 adds post-processing for recursive file listing, task filtering/grouping, task creation, and template-based note creation — all through the same unified tool.

### Known local-only disclosure (eval)

Write/eval commands pass note content (base64 chunks) as process argv to `obsidian-cli`. argv is visible to other local processes via `ps`/`Get-Process` for the call's lifetime. Content is never secrets-shaped by design — still, on shared hosts, prefer `eval file=` (vault note) or `create-from-template` over inline `eval code=` with sensitive content. Upstream stdin/file transport would remove this entirely (tracked in issue #20 L2).

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for release history.

## License

MIT
