feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s

- Add Docker image for the official Obsidian desktop app and CLI.

- Start Obsidian headlessly with Xvfb and DBus setup.

- Expose a safe structured HTTP command API without shell execution.

- Add JWT-based vault, path, and command authorization.

- Support single-vault and multi-vault container mounts.

- Add TypeScript SDK helpers for Obsidian CLI commands.

- Add n8n community node package with Obsidian operations.

- Add docs, compose config, tests, and production image workflow.
This commit is contained in:
2026-06-24 09:11:13 +02:00
commit 902a3df8b5
34 changed files with 5153 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
# Obsidian API
Docker service that exposes an HTTP API for the **official Obsidian CLI**.
It is meant for:
- n8n custom nodes, HTTP Request nodes, and Code nodes
- Prism / TypeScript agent harnesses
- local automation against a mounted Obsidian vault
## Important: official CLI architecture
This uses the real Obsidian CLI from `obsidian.md/help/cli`.
That CLI is not an npm package. It ships inside the Obsidian desktop app and requires the app to be running. Therefore this container runs:
- official Obsidian desktop app
- Xvfb virtual display
- official `/opt/Obsidian/obsidian-cli` exposed as `obsidian`
- this Starlette HTTP API
Because Electron is required, the image is Debian-based and larger than Alpine.
## Security model
The HTTP API does **not** accept shell commands and does **not** accept plain command strings.
Allowed:
```json
{
"args": ["read", "path=Inbox/Note.md"]
}
```
Rejected:
```json
{
"command": "read path=Inbox/Note.md"
}
```
```json
{
"mode": "shell",
"args": ["ls -la"]
}
```
The service always runs the configured binary directly with `exec` semantics:
```bash
obsidian <args...>
```
No shell is involved.
### JWT access policies
Auth uses signed HS256 JWT bearer tokens. The token itself contains the policy claims.
Create a signing secret:
```bash
export OBSIDIAN_JWT_SECRET="$(openssl rand -base64 48)"
```
Generate a token with the same secret:
```bash
python scripts/create_jwt.py \
--sub n8n \
--vault work \
--path Inbox/n8n/ \
--path Automation.md \
--command create \
--command read \
--command append \
--command search \
--command search:context \
--days 365
```
Set the same secret for the API:
```env
OBSIDIAN_JWT_SECRET=change-this-long-random-secret
```
JWT claims:
```json
{
"sub": "n8n",
"vaults": ["work"],
"paths": ["Inbox/n8n/", "Automation.md"],
"commands": ["create", "read", "append", "search", "search:context"],
"iat": 1760000000,
"exp": 1791536000
}
```
Policy claims:
- `vaults`: allowed Obsidian vault names, e.g. `["work"]`
- `paths`: allowed note/path prefixes, e.g. `["Inbox/n8n/", "Automation.md"]`
- `commands`: allowed official CLI commands, e.g. `["read", "create", "append"]`
- `exp`: token expiry timestamp
- `nbf`: optional not-before timestamp
- `iat`: optional issued-at timestamp
Use `["*"]` for unrestricted access in a dimension.
Example request:
```bash
JWT="$(python scripts/create_jwt.py --sub n8n --vault work --path Inbox/n8n/ --command create --days 365)"
curl -X POST http://localhost:8080/commands \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"args":["vault=work","create","path=Inbox/n8n/Event.md","content=Hello","overwrite"]}'
```
Path restrictions are enforced before the Obsidian CLI process starts. If a token has path restrictions, commands that read/write notes must include an explicit `path=`, `file=`, `folder=`, `name=` or `to=` argument.
## Start
```bash
cp .env.example .env
```
Edit:
```env
OBSIDIAN_JWT_SECRET=change-this-long-random-secret
OBSIDIAN_VAULT_PATH=/absolute/path/to/your/vault
OBSIDIAN_API_PORT=8080
```
### Multiple vaults
Single vault is the default:
```env
OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main
```
For more vaults, mount a parent folder and register container paths:
```env
OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main
OBSIDIAN_VAULTS_ROOT=/home/daniel/Documents/Obsidian
OBSIDIAN_VAULTS=main=/vault,work=/vaults/Work,personal=/vaults/Personal
```
Then target a vault with the official CLI prefix:
```json
{
"args": ["vault=work", "read", "path=Inbox/Note.md"]
}
```
If no `vault=<name>` prefix is provided, the first vault in `OBSIDIAN_VAULTS` is used as the default.
Start:
```bash
docker compose up -d --build
```
Logs:
```bash
docker compose logs -f
```
## API
### Health
```bash
curl http://localhost:8080/health
```
### Run official Obsidian CLI command
```bash
curl -X POST http://localhost:8080/commands \
-H "Authorization: Bearer change-me" \
-H "Content-Type: application/json" \
-d '{"args":["version"]}'
```
Create a note:
```bash
curl -X POST http://localhost:8080/commands \
-H "Authorization: Bearer change-me" \
-H "Content-Type: application/json" \
-d '{"args":["create","path=Inbox/Hello.md","content=# Hello","overwrite"]}'
```
Read a note:
```bash
curl -X POST http://localhost:8080/commands \
-H "Authorization: Bearer change-me" \
-H "Content-Type: application/json" \
-d '{"args":["read","path=Inbox/Hello.md"]}'
```
Search:
```bash
curl -X POST http://localhost:8080/commands \
-H "Authorization: Bearer change-me" \
-H "Content-Type: application/json" \
-d '{"args":["search","query=Hello","format=json"]}'
```
## TypeScript SDK
SDK path:
```text
sdk/typescript
```
Install locally:
```bash
npm install ./sdk/typescript
```
Use in TypeScript / Node / Prism:
```ts
import { ObsidianCliClient } from '@obsidian-api/sdk';
const obsidian = new ObsidianCliClient({
baseUrl: 'http://localhost:8080',
token: 'change-me',
});
await obsidian.create({
path: 'Inbox/Hello.md',
content: '# Hello from TypeScript',
overwrite: true,
});
const note = await obsidian.read({ path: 'Inbox/Hello.md' });
console.log(note.stdout);
const results = await obsidian.search('Hello', { format: 'json' });
console.log(JSON.parse(results.stdout));
// Multi-vault: scoped client
const work = obsidian.inVault('work');
await work.create({ path: 'Inbox/Work.md', content: 'Hello work', overwrite: true });
```
n8n options:
- real community-node package: [`packages/n8n-nodes-obsidian-api`](packages/n8n-nodes-obsidian-api)
- Code node / HTTP examples: [`docs/n8n.md`](docs/n8n.md)
Raw escape hatch, still safe because it sends an args array:
```ts
await obsidian.runRaw('daily:append', 'content=- [ ] Buy groceries');
```
## Official CLI command examples
The SDK covers the official command groups from `obsidian.md/help/cli`, including:
- General: `help`, `version`, `reload`, `restart`
- Files: `create`, `read`, `append`, `prepend`, `move`, `rename`, `delete`, `files`, `folders`
- Daily notes: `daily`, `daily:path`, `daily:read`, `daily:append`, `daily:prepend`
- Search: `search`, `search:context`, `search:open`
- Links: `backlinks`, `links`, `unresolved`, `orphans`, `deadends`
- Tags/tasks/properties/templates
- Plugins/themes/snippets
- Sync/Publish commands
- Workspace/tabs/recents
- Developer commands: `dev:*`, `eval`
## Development
Python API tests:
```bash
uv run pytest -q
```
TypeScript SDK typecheck:
```bash
cd sdk/typescript
npm install
npm run typecheck
```