Hacky Relay onboarding

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.

Prerequisites

What the script does

  1. Checks Obsidian is running; launches it if not.
  2. Checks the Relay plugin is loaded; installs it via obsidian plugin:install if missing.
  3. Triggers the plugin's own OAuth flow via obsidian eval — browser opens, you click "Authorize" once. Plugin handles token persistence (we don't need adoptCliToken()).
  4. Creates a Relay workspace (if you don't have one) and a shared folder, all in a single batched eval.
  5. Drops a small AGENTS.md template into the folder root.
  6. Fetches the share key via getRelayInvitation() and prints it for handoff.

The script

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')\""

What's happening at each step

What you DON'T get

Debug: introspect plugin 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)
"

Joining a relay from the other side

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.