feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
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:
@@ -0,0 +1,83 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user