#!/usr/bin/env bash # # thespin.ad — interactive installer. # curl -fsSL https://thespin.ad/install.sh | bash # # Asks which agent to install for and (unless baked in) your publisher key, then # wires up the sponsored line. Reads answers from the terminal even when piped. set -euo pipefail DWELL_URL="https://thespin.ad" KEY="" VSIX_URL="https://thespin.ad/vsix" cyan() { printf '\033[38;5;81m%s\033[0m\n' "$1"; } green() { printf '\033[38;5;156m%s\033[0m\n' "$1"; } dim() { printf '\033[2m%s\033[0m\n' "$1"; } warn() { printf '\033[38;5;221m%s\033[0m\n' "$1"; } # Prompt on the controlling terminal so it works under `curl ... | bash`. ask() { local prompt="$1" default="${2:-}" answer="" if [ -r /dev/tty ]; then read -r -p "$prompt" answer /dev/null || true fi printf '%s' "${answer:-$default}" } install_claude() { local dir="${HOME}/.claude" dst settings tmp dst="${dir}/dwell-statusline.sh" settings="${dir}/settings.json" mkdir -p "$dir" cat > "$dst" <<'THESPIN_STATUSLINE_EOF' #!/usr/bin/env bash # # Dwell — Claude Code status line ad server. # # Prints the current top-bid sponsored line into Claude Code's status line. # Claude Code pipes session JSON on stdin (ignored here) and renders our stdout. # Wire it up in ~/.claude/settings.json (see scripts/install-claude.sh): # # "statusLine": { "type": "command", # "command": "~/.claude/dwell-statusline.sh", # "refreshInterval": 5 } # # Each invocation hits the Dwell serve API, which counts one impression. set -euo pipefail # Drain Claude's JSON payload so the pipe doesn't break (we don't need it). cat >/dev/null 2>&1 || true DWELL_URL="${DWELL_URL:-https://thespin.ad}" # Your publisher key (from the Payouts page). When set, impressions + clicks # this status line shows earn your revenue share. Leave blank to show ads anon. DWELL_KEY="${DWELL_KEY:-}" # ANSI: dim label, bright line, amber price. Claude renders these. DIM=$'\033[2m'; RESET=$'\033[0m'; LIME=$'\033[38;5;156m'; AMBER=$'\033[38;5;221m' # Send the publisher key as a header so the server can attribute earnings. if [ -n "$DWELL_KEY" ]; then resp="$(curl -fsS --max-time 2 -H "X-Dwell-Key: ${DWELL_KEY}" "${DWELL_URL}/api/serve" 2>/dev/null || true)" else resp="$(curl -fsS --max-time 2 "${DWELL_URL}/api/serve" 2>/dev/null || true)" fi if [ -z "$resp" ]; then # API unreachable — stay quiet rather than break the status line. printf '%s' "${DIM}◆ dwell${RESET}" exit 0 fi # Parse with jq when available, else fall back to python3 (ships with macOS). if command -v jq >/dev/null 2>&1; then line="$(printf '%s' "$resp" | jq -r '.ad.line // ""')" brand="$(printf '%s' "$resp" | jq -r '.ad.brand // ""')" price="$(printf '%s' "$resp" | jq -r '.ad.price_per_1k // ""')" elif command -v python3 >/dev/null 2>&1; then read -r line brand price < <(printf '%s' "$resp" | python3 -c ' import sys, json d = (json.load(sys.stdin).get("ad") or {}) print(d.get("line",""), "\x1f", d.get("brand",""), "\x1f", d.get("price_per_1k","")) ' 2>/dev/null | tr "\x1f" " ") else printf '%s' "${DIM}◆ dwell (install jq for ads)${RESET}" exit 0 fi if [ -z "${line:-}" ]; then printf '%s' "${DIM}◆ dwell · spinner unclaimed — bid at ${DWELL_URL}${RESET}" exit 0 fi # e.g. ◆ Ramp · save time and money $25.00/1k [ad] printf '%s◆%s %s%s%s %s%s%s %s[ad]%s' \ "$LIME" "$RESET" "$LIME" "$line" "$RESET" "$AMBER" "${price}/1k" "$RESET" "$DIM" "$RESET" THESPIN_STATUSLINE_EOF chmod +x "$dst" green "✓ Installed $dst" if command -v jq >/dev/null 2>&1; then [ -f "$settings" ] || echo '{}' > "$settings" cp "$settings" "${settings}.thespin-backup" 2>/dev/null || true tmp="$(mktemp)" jq --arg cmd "$dst" --arg url "$DWELL_URL" --arg key "$KEY" ' .statusLine = { "type": "command", "command": $cmd, "refreshInterval": 5 } | .env = ((.env // {}) + { "DWELL_URL": $url } + (if $key == "" then {} else { "DWELL_KEY": $key } end)) | .spinnerVerbs = { "mode": "append", "verbs": ["Sponsored by your highest bid", "Monetizing the wait", "Discombobulating"] } ' "$settings" > "$tmp" && mv "$tmp" "$settings" green "✓ Wired statusLine into $settings" else warn "jq not found — add this to $settings manually:" printf ' "statusLine": { "type": "command", "command": "%s", "refreshInterval": 5 }\n' "$dst" printf ' "env": { "DWELL_URL": "%s", "DWELL_KEY": "%s" }\n' "$DWELL_URL" "$KEY" fi green "Done. Open a new Claude Code session — the spinner is now sponsored." } install_vscode() { local vsix found=0 cli vsix="$(mktemp)" || return 1 if ! curl -fsSL "$VSIX_URL" -o "$vsix"; then warn "Couldn't download the extension from $VSIX_URL"; return 1 fi for cli in code cursor; do if command -v "$cli" >/dev/null 2>&1; then "$cli" --install-extension "$vsix" && { green "✓ Installed into $cli"; found=1; } fi done rm -f "$vsix" if [ "$found" = 0 ]; then warn "No 'code' or 'cursor' CLI found. In VS Code/Cursor run: Shell Command: Install 'code'/'cursor' command, then re-run." return 1 fi [ -n "$KEY" ] && dim "Set thespin.publisherKey to: $KEY (Settings → search 'thespin')" \ || dim "Set thespin.publisherKey in Settings to earn your share." } install_codex() { if ! command -v codex >/dev/null 2>&1; then warn "Codex CLI not found. Install it: npm i -g @openai/codex"; return 1 fi if [ -n "$KEY" ]; then codex mcp add thespin --env DWELL_URL="$DWELL_URL" --env DWELL_KEY="$KEY" -- npx -y @shaferllc/thespin-mcp else codex mcp add thespin --env DWELL_URL="$DWELL_URL" -- npx -y @shaferllc/thespin-mcp fi green "✓ Registered the thespin MCP server with Codex." dim "Add the AGENTS.md nudge so the agent calls sponsor_message at task start." } install_gemini() { if ! command -v gemini >/dev/null 2>&1; then warn "Gemini CLI not found. Install it: npm i -g @google/gemini-cli"; return 1 fi gemini extensions install https://github.com/shaferllc/thespin-extensions --path gemini-cli green "✓ Installed the thespin Gemini extension." [ -n "$KEY" ] && dim "Saved. Set/verify your key: gemini extensions config thespin" \ || dim "Set your key to earn: gemini extensions config thespin" } # Open a URL in the user's browser, best-effort across platforms. open_url() { if command -v open >/dev/null 2>&1; then open "$1" >/dev/null 2>&1 || true elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$1" >/dev/null 2>&1 || true fi } # Browser login handshake — sets the global KEY on success. The installer can't # show a login form, so we pair through the browser like `gh auth login`. browser_login() { local resp code secret verify poll status got i resp="$(curl -fsS "${DWELL_URL}/install/pair" 2>/dev/null)" || { warn "Couldn't reach ${DWELL_URL} — installing anonymously."; return 0; } code="$(printf '%s\n' "$resp" | sed -n 's/^code=//p')" secret="$(printf '%s\n' "$resp" | sed -n 's/^secret=//p')" verify="$(printf '%s\n' "$resp" | sed -n 's/^verify_url=//p')" if [ -z "$code" ] || [ -z "$secret" ] || [ -z "$verify" ]; then warn "Login isn't available right now — installing anonymously."; return 0 fi echo cyan "Log in or sign up to bake in your key:" echo " $verify" open_url "$verify" dim "Waiting for you to authorize in the browser… (Ctrl-C to skip)" # Poll for up to ~3 minutes (90 × 2s). for i in $(seq 1 90); do sleep 2 poll="$(curl -fsS -H "X-Pair-Secret: ${secret}" "${DWELL_URL}/install/pair/${code}" 2>/dev/null)" || continue status="$(printf '%s\n' "$poll" | sed -n 's/^status=//p')" case "$status" in approved) got="$(printf '%s\n' "$poll" | sed -n 's/^key=//p')" if [ -n "$got" ]; then KEY="$got"; green "✓ Linked — your key is baked in."; else warn "Linked, but no key returned — installing anonymously."; fi return 0 ;; expired|denied) warn "Login link expired — installing anonymously."; return 0 ;; esac done warn "Timed out waiting for login — installing anonymously." } cyan "thespin.ad — sponsor your AI spinner, keep 50% of the revenue." echo echo "Install for which agent?" echo " 1) Claude Code native status line (recommended)" echo " 2) Cursor / VS Code status bar" echo " 3) Codex CLI sponsor tool" echo " 4) Gemini CLI sponsor tool" choice="$(ask 'Choose [1]: ' 1)" if [ -z "$KEY" ]; then echo dim "Earn your 50% share — link this install to your account." echo " 1) Log in / sign up in the browser (recommended)" echo " 2) Paste a publisher key" echo " 3) Skip — show ads anonymously" auth_choice="$(ask 'Choose [1]: ' 1)" case "$auth_choice" in 2) KEY="$(ask 'Publisher key: ' '')" ;; 3) : ;; *) browser_login ;; esac fi echo case "$choice" in 2) install_vscode ;; 3) install_codex ;; 4) install_gemini ;; *) install_claude ;; esac