#!/bin/sh # parley join — terminal-native onboarding. No email, no browser, no dashboard. # # curl -fsSL https://parley.aikaara.com/join | sh # # What it does (read it, it's short): # 1. finds your SSH key (~/.ssh/id_ed25519 by default; PARLEY_KEY=path to pick another) # 2. asks parley for a nonce, signs it with `ssh-keygen -Y sign` (proof you hold the key) # 3. parley checks the key is on github.com/.keys → your GitHub login is your identity # 4. prints your MCP token once, and installs it into Claude Code if `claude` is on PATH # Nothing leaves your machine except: your GitHub login, your PUBLIC key, and the signature. set -eu API="${PARLEY_API:-https://api.aikaara.com/api/v1/mcp}" KEY="${PARLEY_KEY:-${SHIPD_KEY:-}}" GH="${PARLEY_GITHUB:-${SHIPD_GITHUB:-${1:-}}}" say() { printf '%s\n' "$*" >&2; } die() { say "parley: $*"; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "needs $1"; } need ssh-keygen; need curl json() { python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get(sys.argv[1],""))' "$1" 2>/dev/null || true; } command -v python3 >/dev/null 2>&1 || die "needs python3 (for json)" # 1. github login (needed first — we pick the local key that is actually on the account) if [ -z "$GH" ]; then GH="$(gh api user -q .login 2>/dev/null || true)" [ -z "$GH" ] && GH="$(git config --get github.user 2>/dev/null || true)" fi if [ -z "$GH" ]; then printf 'github login: ' >&2; read -r GH )" GHRAW="$(curl -fsS "https://github.com/$GH.keys" 2>/dev/null)" || die "github.com/$GH.keys not reachable (typo in login, or offline?)" GHKEYS="$(printf '%s\n' "$GHRAW" | cut -d' ' -f2)" [ -n "$GHKEYS" ] || die "github.com/$GH has no SSH keys. add one at github.com/settings/keys, re-run." # 2. key: SHIPD_KEY if given; else the first local key (~/.ssh/*.pub, then the agent) that GitHub knows PUB=""; SIGNKEY="" pick() { # $1 = candidate pub line → sets PUB if GitHub has it k="$(printf '%s' "$1" | cut -d' ' -f2)"; [ -n "$k" ] || return 1 printf '%s\n' "$GHKEYS" | grep -qxF "$k" && PUB="$(printf '%s' "$1" | cut -d' ' -f1,2)" } if [ -n "$KEY" ]; then [ -f "$KEY" ] || die "PARLEY_KEY=$KEY not found" if [ -f "$KEY.pub" ]; then PUB="$(cut -d' ' -f1,2 "$KEY.pub")"; else PUB="$(ssh-keygen -y -f "$KEY" /dev/null | cut -d' ' -f1,2)"; fi [ -n "$PUB" ] || die "could not read a public key for $KEY" printf '%s\n' "$GHKEYS" | grep -qxF "$(printf '%s' "$PUB" | cut -d' ' -f2)" || die "$KEY is not on github.com/$GH.keys" else for f in "$HOME"/.ssh/*.pub; do [ -f "$f" ] || continue if pick "$(cat "$f")"; then KEY="${f%.pub}"; break; fi done if [ -z "$PUB" ] && command -v ssh-add >/dev/null 2>&1; then OLDIFS=$IFS; IFS=' '; for line in $(ssh-add -L 2>/dev/null); do if pick "$line"; then KEY=""; break; fi; done; IFS=$OLDIFS fi fi if [ -z "$PUB" ]; then say "none of your local keys are on github.com/$GH.keys:" for f in "$HOME"/.ssh/*.pub; do [ -f "$f" ] && say " $(ssh-keygen -lf "$f" | awk '{print $2}') $f"; done die "add one at github.com/settings/keys (pbcopy < ~/.ssh/id_ed25519.pub), or pick one: PARLEY_KEY=~/.ssh/ curl … | sh" fi FP="$(printf '%s' "$PUB" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')" # signing: through the agent when it holds the key (-f ⇒ agent), else the private key with a tty prompt TMPPUB="$HOME/.parley.pub.tmp"; trap 'rm -f "$TMPPUB"' EXIT INT TERM printf '%s\n' "$PUB" > "$TMPPUB" if command -v ssh-add >/dev/null 2>&1 && ssh-add -L 2>/dev/null | grep -qF "$(printf '%s' "$PUB" | cut -d' ' -f2)"; then SIGNKEY="$TMPPUB" elif [ -n "$KEY" ] && [ -f "$KEY" ]; then SIGNKEY="$KEY" else die "key $FP is on GitHub but not loaded in ssh-agent and no private file found — ssh-add it" fi say "→ github $GH" say "→ key $FP (${KEY:-ssh-agent}) ✔ on github" # 3. challenge → sign → claim body=$(python3 -c 'import json,sys; print(json.dumps({"github":sys.argv[1],"pubkey":sys.argv[2]}))' "$GH" "$PUB") ch=$(curl -fsS -X POST "$API/join/challenge" -H 'Content-Type: application/json' -d "$body") || die "challenge failed" NONCE=$(printf '%s' "$ch" | json nonce); [ -n "$NONCE" ] || die "$(printf '%s' "$ch" | json error)" SIG=$(printf '%s' "$NONCE" | ssh-keygen -Y sign -n shipd-join -f "$SIGNKEY" 2>/dev/null) || { [ "$SIGNKEY" != "$KEY" ] && [ -n "$KEY" ] && SIG=$(printf '%s' "$NONCE" | ssh-keygen -Y sign -n shipd-join -f "$KEY" 2>/dev/tty); } || die "signing failed — ssh-add ${KEY:-your key} (OpenSSH ≥ 8.0 needed)" claim=$(python3 -c 'import json,sys; print(json.dumps({"github":sys.argv[1],"pubkey":sys.argv[2],"signature":sys.argv[3],"product":"parley"}))' "$GH" "$PUB" "$SIG") res=$(curl -sS -X POST "$API/join/claim" -H 'Content-Type: application/json' -d "$claim") TOKEN=$(printf '%s' "$res" | json token); [ -n "$TOKEN" ] || die "$(printf '%s' "$res" | json error)" # 4. output + install say "" say "✔ joined parley as @$GH" WS=$(printf "%s" "$res" | python3 -c 'import sys,json; print((json.load(sys.stdin).get("workspace") or {}).get("tenant_slug",""))' 2>/dev/null); [ -n "$WS" ] && say "workspace $WS" say "" printf '%s\n' "$TOKEN" say "" mkdir -p "$HOME/.parley" && umask 077 && printf "%s\n" "$TOKEN" > "$HOME/.parley/token" && say "saved to ~/.parley/token (0600)" if command -v claude >/dev/null 2>&1; then claude mcp add --transport http parley https://mcp.aikaara.com/parley --header "Authorization: Bearer $TOKEN" >/dev/null 2>&1 \ && say "added to claude code: claude mcp list" || say "add manually: claude mcp add --transport http parley https://mcp.aikaara.com/parley --header \"Authorization: Bearer \"" else say "mcp config: {\"mcpServers\":{\"parley\":{\"url\":\"https://mcp.aikaara.com/parley\",\"headers\":{\"Authorization\":\"Bearer \"}}}}" fi say "" say "next: tell your agent “create a parley agent that …” — it goes live on the web instantly" say "chat: curl -fsSL https://parley.aikaara.com/chat | sh (talk to the parley team, terminal to terminal)"