Files
daniel156161 902a3df8b5
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
feat: add Obsidian CLI API service
- 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.
2026-06-24 09:11:13 +02:00

84 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""Render the global Obsidian desktop config (~/.config/obsidian/obsidian.json).
Enables the official CLI and registers every mounted vault so the official
`obsidian` CLI can target them with `vault=<name>`.
Vaults are read from the environment:
* ``OBSIDIAN_VAULTS`` (preferred, multi-vault):
- JSON object: {"work": "/vaults/work", "personal": "/vaults/personal"}
- or pairs: work=/vaults/work,personal=/vaults/personal
* ``OBSIDIAN_VAULT_PATH`` (single-vault fallback): /vault
The first registered vault becomes the default (highest ``ts``), which is the
vault used when a CLI command omits ``vault=<name>``.
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
def _parse_vaults() -> "list[tuple[str, str]]":
raw = os.environ.get("OBSIDIAN_VAULTS", "").strip()
pairs: list[tuple[str, str]] = []
if raw:
# Try JSON object first.
try:
data = json.loads(raw)
if isinstance(data, dict):
return [(str(name), str(path)) for name, path in data.items()]
except json.JSONDecodeError:
pass
# Fall back to comma/newline separated name=path pairs.
for chunk in raw.replace("\n", ",").split(","):
chunk = chunk.strip()
if not chunk:
continue
if "=" not in chunk:
print(f"WARNING: ignoring malformed OBSIDIAN_VAULTS entry: {chunk!r}", file=sys.stderr)
continue
name, path = chunk.split("=", 1)
pairs.append((name.strip(), path.strip()))
if not pairs:
single = os.environ.get("OBSIDIAN_VAULT_PATH", "/vault").strip() or "/vault"
name = Path(single).name or "vault"
pairs.append((name, single))
return pairs
def main() -> int:
pairs = _parse_vaults()
vaults: dict[str, dict] = {}
# Descending timestamps so the first declared vault sorts as most-recent
# (the desktop app treats the highest ts as the active/default vault).
base_ts = 1893456000000
for index, (name, path) in enumerate(pairs):
vaults[name] = {
"path": path,
"ts": base_ts - index,
"open": True,
}
config = {"cli": True, "vaults": vaults}
config_path = Path(os.environ.get("OBSIDIAN_CONFIG_PATH", "/root/.config/obsidian/obsidian.json"))
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
names = ", ".join(f"{name} -> {data['path']}" for name, data in vaults.items())
print(f"Registered {len(vaults)} vault(s): {names}")
# First declared vault is the default for commands without vault=<name>.
print(pairs[0][1]) # default vault path, consumed by the entrypoint
return 0
if __name__ == "__main__":
raise SystemExit(main())