902a3df8b5
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.
272 lines
6.4 KiB
Markdown
272 lines
6.4 KiB
Markdown
# 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
|
|
```
|