This is the demo path that runs today using only what already ships in the v0.7.4 Relay plugin. The real CLI is at getrelay.slophouse.md; this is the bridge while it's being built.
It works by calling the plugin's own internal methods directly via obsidian eval. No new code on Dan's side. No real relay CLI required beyond the stub that's already installed.
Run against a fresh test vault first. This script logs into Relay, creates a workspace, and adds a shared folder. If your existing Obsidian vault is already signed in to a different Relay account, this will overwrite that state mid-flow and weird things will happen. Create a clean test vault before running.
obsidian plugin:install if missing.obsidian eval — browser opens, you click "Authorize" once. Plugin handles token persistence (we don't need adoptCliToken()).AGENTS.md template into the folder root.getRelayInvitation() and prints it for handoff.Save as relay-demo.sh, chmod +x relay-demo.sh, run with an optional vault-relative path argument:
#!/usr/bin/env bash
# relay-demo.sh — hacky agent-driven onboarding harness.
# Uses only existing Relay plugin v0.7.4+ primitives via `obsidian eval`.
# No new Dan-side code required. Mac-only.
set -e
PLUGIN='app.plugins.plugins["system3-relay"]'
FOLDER_REL="${1:-Notes/relay-demo}" # vault-relative path
# === 1. Pre-flight: Obsidian running + plugin loaded ===
if ! pgrep -f /Applications/Obsidian.app >/dev/null; then
echo "Launching Obsidian..."
open -a Obsidian
sleep 5
fi
LOADED=$(obsidian eval code="Object.keys(app.plugins.plugins).includes('system3-relay')")
if [ "$LOADED" != "true" ]; then
echo "Installing Relay plugin..."
obsidian plugin:install id=system3-relay enable
sleep 3
fi
# === 2. Login (browser opens; user clicks Authorize once) ===
LOGGED_IN=$(obsidian eval code="${PLUGIN}.loginManager.loggedIn")
if [ "$LOGGED_IN" != "true" ]; then
echo "Opening browser for Relay sign-in. Click 'Authorize' when prompted..."
obsidian eval code="${PLUGIN}.loginManager.login('google')" &
for _ in {1..60}; do
sleep 1
L=$(obsidian eval code="${PLUGIN}.loginManager.loggedIn" 2>/dev/null || echo false)
[ "$L" = "true" ] && break
done
fi
USER_EMAIL=$(obsidian eval code="${PLUGIN}.loginManager.user?.email || 'unknown'")
echo "Logged in as $USER_EMAIL"
# === 3. Create relay + shared folder ===
VAULT=$(obsidian eval code='app.vault.adapter.basePath')
mkdir -p "$VAULT/$FOLDER_REL"
# Make sure Obsidian sees the folder
obsidian eval code="if (!(await app.vault.adapter.exists('${FOLDER_REL}'))) await app.vault.createFolder('${FOLDER_REL}')" >/dev/null
RESULT=$(obsidian eval code="
(async () => {
const plugin = ${PLUGIN};
let relay = Array.from(plugin.relayManager.relays.values())[0];
if (!relay) relay = await plugin.relayManager.createRelay('Demo Workspace');
const folderGuid = crypto.randomUUID();
// sharedFolders.new(path, folderGuid, relay.guid, authoritative=true)
// — note relay.guid (UUID), not relay.id (PocketBase record ID)
const local = plugin.sharedFolders.new('${FOLDER_REL}', folderGuid, relay.guid, true);
const remote = await plugin.relayManager.createRemoteFolder(local, relay);
return JSON.stringify({relayId: relay.id, folderId: remote.id});
})()
")
RELAY_ID=$(echo "$RESULT" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['relayId'])")
echo "Shared folder created at $VAULT/$FOLDER_REL (relay $RELAY_ID)"
# === 4. Drop AGENTS.md ===
cat > "$VAULT/$FOLDER_REL/AGENTS.md" <<'EOF'
# This is a Relay multiplayer folder
Files here sync in real time to other Relay users.
- Files may change while you're reading or editing them.
- Your edits propagate to other users immediately.
- Prototype build — see https://hackygetrelay.slophouse.md
EOF
# === 5. Print share key for handoff ===
SHARE_KEY=$(obsidian eval code="
(async () => {
const rm = ${PLUGIN}.relayManager;
const relay = rm.relays.get('${RELAY_ID}');
const invite = await rm.getRelayInvitation(relay);
return invite.key;
})()
")
echo ""
echo "=== Done. ==="
echo "Folder: $VAULT/$FOLDER_REL"
echo "Share key: $SHARE_KEY"
echo ""
echo "Send the share key to your collaborator. They run on their Mac:"
echo " obsidian eval code=\"await app.plugins.plugins['system3-relay'].relayManager.acceptInvitation('$SHARE_KEY')\""
pgrep and open -a Obsidian to ensure the app is running. The Obsidian CLI is a remote-control surface; it talks to the running app via IPC. If Obsidian isn't running, eval commands hang.loginManager.login('google') directly via eval. The plugin's own OAuth opens a browser, waits for "Authorize," and persists the token to its internal authStore. The script polls until loginManager.loggedIn flips true.sharedFolders.new(path, folderGuid, relay.guid, true) → promote to server-side via createRemoteFolder(local, relay). The local-folder method takes a caller-generated UUID and uses relay.guid (the plugin-side UUID), not relay.id (the PocketBase record ID). Verified end-to-end against the harness's arlo slot on 2026-05-15.AGENTS.md stub into the folder root. The plugin's CRDT layer propagates it to other devices automatically.getRelayInvitation(relay) which returns an existing invitation or creates one. Prints the key string.relay command. Everything goes through obsidian eval. The brew-installed relay binary is just a placeholder right now.relay status-style CLI. See the debug eval below if you need to introspect state.Useful as a poor-man's relay status:
obsidian eval code="
JSON.stringify({
loggedIn: app.plugins.plugins['system3-relay'].loginManager.loggedIn,
user: app.plugins.plugins['system3-relay'].loginManager.user?.email,
relays: Array.from(app.plugins.plugins['system3-relay'].relayManager.relays.values()).map(r => ({id: r.id, name: r.name})),
folders: Array.from(app.plugins.plugins['system3-relay'].sharedFolders).map(f => ({path: f.path, guid: f.guid}))
}, null, 2)
"
When the script prints a share key, the recipient runs (on their own Mac with Obsidian running and Relay loaded):
obsidian eval code="await app.plugins.plugins['system3-relay'].relayManager.acceptInvitation('relay-7f3a91c2')"
Their folder appears, auto-attached, and starts syncing.