Files
chainbase-labs__agentkey/scripts/install.sh
T
Bruce ab0dba7eb1 feat: rename @agentkey/mcp → @agentkey/cli in install scripts and docs (#47)
## Summary

Companion change to
[chainbase-labs/AgentKey-Server#32](https://github.com/chainbase-labs/AgentKey-Server/pull/32).
The npm package has been renamed; this PR updates everything users
actually see.

**User-facing**
- \`scripts/install.{sh,ps1}\`: \`MCP_PACKAGE\` / \`$McpPackage\` →
\`CLI_PACKAGE\` / \`$CliPackage\`
- \`scripts/uninstall.{sh,ps1}\`: sweep BOTH \`@agentkey/cli\` and
\`@agentkey/mcp\` so users upgrading from v0.x get a clean uninstall
- \`skills/agentkey/SKILL.md\`: setup command + manual-config JSON
example
- \`README.md\` + \`docs/README_zh.md\`: every \`npx @agentkey/mcp\`
command + the manual-config JSON examples (switched from stale stdio
shape to the remote-HTTP shape that \`--auth-login\` actually writes in
v1.0)

**Internal / protocol docs**
- \`SECURITY.md\`: file/network audit tables
- \`protocol/skill-meta-v1.md\`, \`docs/SERVER-IMPLEMENTATION.md\`:
clarify the protocol partner is AgentKey-Server's hosted \`/v1/mcp\`
endpoint, not a standalone npm package — these references were already
misleading before the rename
- \`.claude/CLAUDE.md\`: keep Claude-session guidance in sync

\`CHANGELOG.md\` left untouched (release-please manages history).

## Sequencing

Merge this PR **after** \`@agentkey/cli\` is published to npm —
otherwise the install scripts here will reference a package that doesn't
exist yet. Recommended order:

1. Merge
[AgentKey-Server#32](https://github.com/chainbase-labs/AgentKey-Server/pull/32)
2. \`cd cli && npm publish\` (first \`@agentkey/cli\` release at
\`1.0.0\`)
3. Merge this PR

## Test plan

- [ ] Verify \`npx -y @agentkey/cli --auth-login\` works end-to-end
after npm publish
- [ ] Verify \`scripts/uninstall.sh\` on a machine with
\`@agentkey/mcp\` globally installed cleans it up
- [ ] Spot-check the rendered README on GitHub for any missed references
2026-05-15 16:31:42 +08:00

561 lines
25 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# AgentKey installer for macOS and Linux
# Usage: curl -fsSL https://agentkey.app/install.sh | bash
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --yes
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --interactive
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --only claude-code,cursor
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --remote
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-mcp
#
# The whole procedural body is wrapped in `main()` so that under `curl | bash`
# bash reads the entire script into memory (as a function definition) before
# executing any of it. Without this wrapper, `exec < /dev/tty` would clobber
# bash's own script-source fd and the shell would hang trying to read the rest
# of itself from the terminal.
set -euo pipefail
# ── Constants ─────────────────────────────────────────────────────────────
SKILL_REPO="chainbase-labs/agentkey"
CLI_PACKAGE="@agentkey/cli"
NODE_MIN_MAJOR=18
# ── Agent markers ─────────────────────────────────────────────────────────
# Subset of vercel-labs/skills' 45 supported agent IDs that have reliable
# on-disk markers (config dirs / binaries on PATH). Agents we can't probe
# cleanly (mostly VS Code extensions like cline/continue/roo) just don't get
# pre-detected — the user can pass --all-agents or --only to include them.
# Sync source: https://github.com/vercel-labs/skills (Supported Agents table).
#
# Format: <agent-id>|<marker>[,<marker>...]
# marker types: cmd:foo — `command -v foo`
# path:/abs/or/~path — file or dir exists (~ expands to $HOME)
AGENT_MARKERS=(
"claude-code|path:~/.claude.json,cmd:claude,path:~/Library/Application Support/Claude,path:~/.config/Claude"
"cursor|path:~/.cursor,cmd:cursor"
"codex|path:~/.codex,cmd:codex"
"gemini-cli|path:~/.gemini,cmd:gemini"
"opencode|path:~/.opencode,cmd:opencode"
"openclaw|path:~/.openclaw"
"qwen-code|path:~/.qwen,cmd:qwen"
"iflow-cli|path:~/.iflow,cmd:iflow"
"windsurf|path:~/.windsurf,cmd:windsurf"
"warp|path:~/.warp,path:~/Library/Application Support/dev.warp.Warp-Stable"
"amp|cmd:amp"
"crush|cmd:crush"
"goose|cmd:goose"
"droid|cmd:droid"
"kode|cmd:kode"
"kilo|cmd:kilo"
"kimi-cli|path:~/.kimi,cmd:kimi"
"kiro-cli|path:~/.kiro,cmd:kiro"
)
# ── Colors (only if stdout is a TTY) ─────────────────────────────────────
# Use $'...' so variables hold real ESC bytes — otherwise heredoc output prints
# the literal string "\033[1m" instead of applying the SGR code.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
BOLD=$'\033[1m'
ACCENT=$'\033[38;2;0;200;180m' # AgentKey teal
INFO=$'\033[38;2;136;146;176m'
SUCCESS=$'\033[38;2;0;220;150m'
WARN=$'\033[38;2;255;176;32m'
ERROR=$'\033[38;2;230;57;70m'
MUTED=$'\033[38;2;110;118;132m'
NC=$'\033[0m'
else
BOLD=''; ACCENT=''; INFO=''; SUCCESS=''; WARN=''; ERROR=''; MUTED=''; NC=''
fi
# ── UI helpers ────────────────────────────────────────────────────────────
ui_banner() {
printf "\n"
printf "${ACCENT} █████ ██████ ███████ ███ ██ ████████ ██ ██ ███████ ██ ██${NC}\n"
printf "${ACCENT} ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ${NC}\n"
printf "${ACCENT} ███████ ██ ███ █████ ██ ██ ██ ██ █████ █████ ████ ${NC}\n"
printf "${ACCENT} ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ${NC}\n"
printf "${ACCENT} ██ ██ ██████ ███████ ██ ████ ██ ██ ██ ███████ ██ ${NC}\n"
printf "\n"
printf " ${BOLD}One command. Full internet access for your AI agent.${NC}\n"
printf " ${MUTED}https://agentkey.app${NC}\n\n"
}
ui_info() { printf " ${INFO}${NC} %s\n" "$*"; }
ui_ok() { printf " ${SUCCESS}${NC} %s\n" "$*"; }
ui_warn() { printf " ${WARN}!${NC} %s\n" "$*"; }
ui_error() { printf " ${ERROR}${NC} %s\n" "$*" >&2; }
ui_step() { printf "\n ${BOLD}%s${NC}\n" "$*"; }
ui_muted() { printf " ${MUTED}%s${NC}\n" "$*"; }
die() { ui_error "$*"; exit 1; }
print_help() {
cat <<EOF
AgentKey installer for macOS and Linux
Usage:
curl -fsSL https://agentkey.app/install.sh | bash
curl -fsSL https://agentkey.app/install.sh | bash -s -- [OPTIONS]
Options:
--yes, -y Non-interactive: install skill to every detected agent, no prompts
--interactive Force interactive mode (fails if no TTY/terminal is reachable)
--only <a,b,c> Only install skill for these agents (comma-separated, e.g. claude-code,cursor)
--all-agents Skip auto-detection; let 'skills' CLI install for every detected agent
--list-agents Print the agents we'd auto-select on this machine and exit
--remote Force remote-install mode: print URL + QR for the auth step,
do NOT auto-open a local browser. Use this when running over
SSH, in Docker, or via OpenClaw / Claude Code remote channels.
--local Force local mode (auto-open browser) and bypass remote heuristics
--force-mcp Re-run MCP auth even if AgentKey is already configured
--skip-skill Skip the skill install step (only run MCP auth)
--skip-mcp Skip the MCP auth step (only install the skill)
--no-telemetry Disable anonymous usage telemetry (writes
~/.config/agentkey/telemetry-disabled so the skill
stays opted-out across runs)
-h, --help Show this help
Behavior:
Interactive mode is the default when a terminal is reachable; otherwise it
falls back to --yes. The installer auto-detects which AI agents are on this
machine and pre-selects them for skill installation. Remote-install mode is
auto-detected from \$HOME/.openclaw, SSH env vars, and missing \$DISPLAY;
override with --remote / --local.
EOF
}
# ── Helpers: agent + remote detection ─────────────────────────────────────
# Expand a leading "~" to \$HOME (no glob expansion, no eval).
_expand_path() {
local p="$1"
case "$p" in
"~"|"~/"*) printf '%s\n' "$HOME${p#"~"}" ;;
*) printf '%s\n' "$p" ;;
esac
}
# Probe a single marker: cmd:NAME (binary on PATH) or path:PATH (file/dir).
_probe_marker() {
local m="$1"
case "$m" in
cmd:*) command -v "${m#cmd:}" >/dev/null 2>&1 ;;
path:*) [ -e "$(_expand_path "${m#path:}")" ] ;;
*) return 1 ;;
esac
}
# Print detected agent IDs as a comma-separated list (empty if none).
detect_agents() {
local entry id markers marker hits=()
for entry in "${AGENT_MARKERS[@]}"; do
id="${entry%%|*}"
markers="${entry#*|}"
# Any marker hit ⇒ agent detected.
IFS=',' read -ra marker_list <<<"$markers"
for marker in "${marker_list[@]}"; do
if _probe_marker "$marker"; then
hits+=("$id")
break
fi
done
done
if [ ${#hits[@]} -gt 0 ]; then
printf '%s\n' "${hits[@]}" | sort -u | paste -sd, -
fi
}
# Detect "remote install" — a context where auto-opening a browser on this
# host is futile because the user isn't sitting in front of it. Mutates
# nothing; returns 0 (true) for remote, 1 (false) for local.
detect_remote() {
[ "$FORCE_LOCAL" = true ] && return 1
[ "$FORCE_REMOTE" = true ] && return 0
# OpenClaw runtime — the project owner confirms ~/.openclaw exists in
# any host where OpenClaw is installed/active. Most reliable single
# signal because it doesn't depend on env-var inheritance through the
# docker → channel → shell chain.
[ -d "$HOME/.openclaw" ] && return 0
# Generic SSH session.
[ -n "${SSH_CONNECTION:-}" ] && return 0
[ -n "${SSH_TTY:-}" ] && return 0
# Headless Linux (no GUI session).
if [ "$(uname -s)" = "Linux" ] \
&& [ -z "${DISPLAY:-}" ] \
&& [ -z "${WAYLAND_DISPLAY:-}" ]; then
return 0
fi
return 1
}
# Cheap "is AgentKey already configured?" check across known MCP config files.
# Greps for an agentkey block + a non-empty AGENTKEY_API_KEY of the expected
# shape. Returns 0 if at least one config has both.
already_authed() {
local cfg
for cfg in "$HOME/.claude.json" \
"$HOME/.cursor/mcp.json" \
"$HOME/Library/Application Support/Claude/claude_desktop_config.json" \
"$HOME/.config/Claude/claude_desktop_config.json"; do
[ -f "$cfg" ] || continue
if grep -q '"agentkey"' "$cfg" 2>/dev/null \
&& grep -qE '"AGENTKEY_API_KEY"[^"]*"ak_[A-Za-z0-9_-]+"' "$cfg" 2>/dev/null; then
return 0
fi
done
return 1
}
install_node() {
local platform="$1"
ui_info "Installing Node.js v$NODE_MIN_MAJOR+ ..."
if [ "$platform" = "macos" ]; then
if command -v brew >/dev/null 2>&1; then
brew install node >/dev/null 2>&1 || die "brew install node failed"
else
die "Homebrew not found. Install Node.js v$NODE_MIN_MAJOR+ manually: https://nodejs.org/"
fi
else
# Linux: NodeSource for apt/dnf/yum; apk for Alpine; otherwise manual
if command -v apt-get >/dev/null 2>&1; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - >/dev/null 2>&1 \
&& sudo apt-get install -y nodejs >/dev/null 2>&1 || die "apt install nodejs failed"
elif command -v dnf >/dev/null 2>&1; then
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo -E bash - >/dev/null 2>&1 \
&& sudo dnf install -y nodejs >/dev/null 2>&1 || die "dnf install nodejs failed"
elif command -v yum >/dev/null 2>&1; then
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo -E bash - >/dev/null 2>&1 \
&& sudo yum install -y nodejs >/dev/null 2>&1 || die "yum install nodejs failed"
elif command -v apk >/dev/null 2>&1; then
sudo apk add --no-cache nodejs npm >/dev/null 2>&1 || die "apk add nodejs failed"
else
die "No supported package manager found. Install Node.js v$NODE_MIN_MAJOR+ manually: https://nodejs.org/"
fi
fi
ui_ok "Node.js installed"
}
# Compute a stable per-device fingerprint for install_completed dedup.
# spec §6.3: sha256(hostname+platform+username)[:16]. Falls back to a random
# value if neither sha256sum nor shasum is available (extremely rare).
compute_device_fingerprint() {
local platform="$1"
local hn user input hash
hn="$(hostname 2>/dev/null || echo "")"
user="${USER:-$(id -un 2>/dev/null || echo "")}"
input="$hn|$platform|$user"
if command -v sha256sum >/dev/null 2>&1; then
hash="$(printf '%s' "$input" | sha256sum | cut -c1-16)"
elif command -v shasum >/dev/null 2>&1; then
hash="$(printf '%s' "$input" | shasum -a 256 | cut -c1-16)"
else
# Last resort: use $RANDOM. Won't dedup across runs but won't crash.
hash="rnd$(printf '%04x%04x%04x' "$RANDOM" "$RANDOM" "$RANDOM")"
fi
printf '%s' "$hash"
}
# ──────────────────────────────────────────────────────────────────────────
# main — wraps the entire procedural body so that under `curl | bash`
# bash finishes reading the script before any fd-rebinding happens.
# ──────────────────────────────────────────────────────────────────────────
main() {
local MODE=""
local ONLY_AGENTS=""
local SKIP_MCP=false
local SKIP_SKILL=false
local PRINT_HELP=false
local LIST_AGENTS=false
local ALL_AGENTS=false
local FORCE_MCP=false
# FORCE_REMOTE / FORCE_LOCAL are read by detect_remote(). Declared as
# plain (non-`local`) so the helpers see them — they're dynamic-scope
# accessible either way in bash, but explicit assignment here keeps
# `set -u` happy.
FORCE_REMOTE=false
FORCE_LOCAL=false
local NO_TELEMETRY=false
# Snapshot original args before the parse loop shifts them away — needed
# later for AGENTKEY_INSTALLER_FLAGS env passthrough.
local _orig_args=("$@")
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes) MODE=noninteractive; shift ;;
--interactive) MODE=interactive; shift ;;
--only) ONLY_AGENTS="${2:-}"; shift 2 ;;
--only=*) ONLY_AGENTS="${1#*=}"; shift ;;
--all-agents) ALL_AGENTS=true; shift ;;
--list-agents) LIST_AGENTS=true; shift ;;
--remote) FORCE_REMOTE=true; shift ;;
--local) FORCE_LOCAL=true; shift ;;
--force-mcp) FORCE_MCP=true; shift ;;
--skip-skill) SKIP_SKILL=true; shift ;;
--skip-mcp) SKIP_MCP=true; shift ;;
--no-telemetry) NO_TELEMETRY=true; shift ;;
-h|--help) PRINT_HELP=true; shift ;;
*) ui_warn "Unknown argument: $1"; shift ;;
esac
done
if $PRINT_HELP; then print_help; exit 0; fi
if $FORCE_REMOTE && $FORCE_LOCAL; then
die "--remote and --local are mutually exclusive"
fi
if $LIST_AGENTS; then
local detected
detected="$(detect_agents)"
if [ -n "$detected" ]; then
printf '%s\n' "$detected" | tr ',' '\n'
else
printf 'no agents detected on this host\n' >&2
fi
exit 0
fi
ui_banner
# ── 1. Preflight ──────────────────────────────────────────────────────
ui_step "1. Preflight"
local OS PLATFORM
OS="$(uname -s)"
case "$OS" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
*) die "Unsupported OS: $OS (macOS/Linux only; use install.ps1 on Windows)" ;;
esac
ui_ok "Platform: $PLATFORM"
# Resolve stdin. `curl | bash` eats stdin — but /dev/tty is usually still
# reachable. Test by *actually opening* /dev/tty in a subshell; `[ -r ]`
# returns true even when the process has lost its controlling terminal
# (e.g. backgrounded, daemonized).
#
# IMPORTANT: we do NOT `exec < /dev/tty` globally. Under `curl | bash`
# bash is reading the script from its own stdin (the pipe); a global
# rebind would hijack bash's script reader and hang after `main` returns
# (bash would try to read the next byte from /dev/tty instead of EOF).
# Instead we redirect stdin *per interactive command* below.
local TTY_AVAILABLE=false
if ( : < /dev/tty ) >/dev/null 2>&1; then
TTY_AVAILABLE=true
fi
if [ -z "$MODE" ]; then
if $TTY_AVAILABLE; then
MODE=interactive
else
MODE=noninteractive
ui_warn "No terminal detected (CI/non-TTY shell) — falling back to --yes"
fi
elif [ "$MODE" = interactive ] && ! $TTY_AVAILABLE; then
die "--interactive requested but no TTY is reachable"
fi
ui_ok "Mode: $MODE"
# Resolve telemetry intent: --no-telemetry overrides everything; existing
# ~/.config/agentkey/telemetry-disabled file means already-opted-out.
local TELEMETRY_OPT_OUT_FILE="$HOME/.config/agentkey/telemetry-disabled"
if $NO_TELEMETRY; then
mkdir -p "$(dirname "$TELEMETRY_OPT_OUT_FILE")" 2>/dev/null || true
touch "$TELEMETRY_OPT_OUT_FILE" 2>/dev/null || true
ui_ok "Telemetry: disabled (--no-telemetry)"
elif [ -f "$TELEMETRY_OPT_OUT_FILE" ]; then
ui_ok "Telemetry: disabled (~/.config/agentkey/telemetry-disabled exists)"
else
ui_info "Telemetry: anonymous usage stats enabled (re-run with --no-telemetry to opt out)"
fi
# Node check
local NODE_OK=false NODE_VERSION NODE_MAJOR
if command -v node >/dev/null 2>&1; then
NODE_VERSION="$(node --version 2>/dev/null | sed 's/^v//')"
NODE_MAJOR="${NODE_VERSION%%.*}"
if [ -n "$NODE_MAJOR" ] && [ "$NODE_MAJOR" -ge "$NODE_MIN_MAJOR" ] 2>/dev/null; then
NODE_OK=true
ui_ok "Node.js: v$NODE_VERSION"
else
ui_warn "Node.js v$NODE_VERSION found but v$NODE_MIN_MAJOR+ is required"
fi
fi
if ! $NODE_OK; then
if [ "$MODE" = interactive ]; then
printf "\n ${BOLD}Node.js v%s+ is required but not found.${NC}\n" "$NODE_MIN_MAJOR"
printf " Install it now? [Y/n] "
local REPLY=""
# Read directly from the terminal, not from bash's stdin (the pipe)
read -r REPLY < /dev/tty || REPLY=""
case "$REPLY" in
n|N|no|No) die "Node.js required. Aborting." ;;
esac
fi
install_node "$PLATFORM"
fi
command -v npx >/dev/null 2>&1 || die "npx not found after Node install — please reinstall Node.js"
# ── 2. Install the AgentKey skill ─────────────────────────────────────
if ! $SKIP_SKILL; then
ui_step "2. Install the AgentKey skill"
# Resolve target agent list:
# 1. --only wins (manual override)
# 2. else --all-agents ⇒ no -a (let skills CLI auto-detect everything)
# 3. else our auto-detection ⇒ -a <detected list>
# 4. else (nothing detected) ⇒ no -a (fall back to skills CLI default)
local TARGETS=""
if [ -n "$ONLY_AGENTS" ]; then
TARGETS="$ONLY_AGENTS"
ui_info "Targeting agents from --only: $TARGETS"
elif $ALL_AGENTS; then
ui_info "Installing for every agent the 'skills' CLI detects (--all-agents)"
else
TARGETS="$(detect_agents)"
if [ -n "$TARGETS" ]; then
ui_ok "Detected agents on this host: $TARGETS"
ui_muted "(override with --only <ids>, or use --all-agents)"
else
ui_info "No agents auto-detected — letting 'skills' CLI scan."
fi
fi
local SKILLS_ARGS=(-y skills add "$SKILL_REPO" -g)
if [ -n "$TARGETS" ]; then
# `skills` CLI accepts -a as either repeated or comma-separated.
# We pass each ID individually for maximum compatibility.
local AGENT_LIST=()
IFS=',' read -ra AGENT_LIST <<<"$TARGETS"
SKILLS_ARGS+=(-a "${AGENT_LIST[@]}")
fi
# Always pass -y in noninteractive mode AND when we already resolved
# an explicit target list — there's nothing left to ask the user.
if [ "$MODE" = noninteractive ] || [ -n "$TARGETS" ]; then
SKILLS_ARGS+=(-y)
fi
# Route npx's stdin to the terminal so its interactive multi-select can
# prompt the user — otherwise it inherits bash's piped stdin and breaks.
# When non-interactive (no TTY), stdin stays as /dev/null via < /dev/null
# to guarantee npx never blocks waiting for input.
local npx_stdin="/dev/null"
if [ "$MODE" = interactive ] && $TTY_AVAILABLE; then
npx_stdin="/dev/tty"
fi
if ! npx "${SKILLS_ARGS[@]}" < "$npx_stdin"; then
die "Failed to install skill via 'skills' CLI"
fi
# The skills CLI sometimes prints "Installation failed" and still
# exits 0 (e.g. network error during git clone). Verify the skill
# actually landed on disk before declaring success.
local _agentkey_found=false _dir
for _dir in \
"$HOME/.agents/skills/agentkey" \
"$HOME/.claude/skills/agentkey" \
"$HOME/.cursor/skills/agentkey" \
"$HOME/.codex/skills/agentkey" \
"$HOME/.gemini/skills/agentkey" \
"$HOME/.opencode/skills/agentkey" \
"$HOME/.openclaw/skills/agentkey" \
"$HOME/.qwen/skills/agentkey" \
"$HOME/.iflow/skills/agentkey" \
"$HOME/.windsurf/skills/agentkey" \
"$HOME/.warp/skills/agentkey"; do
[ -f "$_dir/SKILL.md" ] && { _agentkey_found=true; break; }
done
if ! $_agentkey_found; then
die "Skill install reported success but no agentkey SKILL.md was created — likely a network or git clone failure. Retry: npx -y skills add $SKILL_REPO -g -y"
fi
ui_ok "Skill installed"
else
ui_step "2. Install the AgentKey skill"
ui_muted "Skipped (--skip-skill)"
fi
# ── 3. MCP authentication ────────────────────────────────────────────
if $SKIP_MCP; then
ui_step "3. Register the MCP server"
ui_muted "Skipped (--skip-mcp)"
elif already_authed && ! $FORCE_MCP; then
ui_step "3. Register the MCP server"
ui_ok "AgentKey is already configured in an MCP client config — skipping auth."
ui_muted "Re-run with --force-mcp to authenticate again."
else
# Decide local-vs-remote and route the MCP CLI flags accordingly.
local IS_REMOTE=false
if detect_remote; then IS_REMOTE=true; fi
local AUTH_ARGS=(--auth-login)
if $IS_REMOTE; then
ui_step "3. Register the MCP server (remote auth: scan QR with phone)"
ui_info "Detected remote install context — printing QR + URL instead of opening a browser here."
if [ -d "$HOME/.openclaw" ]; then
ui_muted " reason: \$HOME/.openclaw exists (OpenClaw runtime)"
elif [ -n "${SSH_CONNECTION:-}" ] || [ -n "${SSH_TTY:-}" ]; then
ui_muted " reason: SSH session detected"
elif [ "$(uname -s)" = "Linux" ]; then
ui_muted " reason: Linux without \$DISPLAY / \$WAYLAND_DISPLAY"
fi
ui_muted "Override with --local if you want a browser opened on this machine instead."
AUTH_ARGS+=(--no-browser)
else
ui_step "3. Register the MCP server (browser login)"
ui_info "Opening your browser for AgentKey device authentication ..."
ui_muted "When auth finishes, the MCP server is written into Claude Code / Claude Desktop / Cursor configs."
fi
echo
# Telemetry context for `install_completed`. Opt-out is honored at
# the SOURCE: when AGENTKEY_TELEMETRY=0, no other context env vars
# are exported — hostname-derived fingerprint, agent lists, and
# installer flags are never computed nor passed to the child
# `npx @agentkey/cli` process. The server treats AGENTKEY_TELEMETRY=0
# as a hard skip.
if $NO_TELEMETRY || [ -f "$TELEMETRY_OPT_OUT_FILE" ]; then
export AGENTKEY_TELEMETRY=0
else
export AGENTKEY_TELEMETRY=1
local _flags=""
for _f in "${_orig_args[@]:-}"; do
_flags="${_flags:+$_flags,}$_f"
done
export AGENTKEY_INSTALL_SOURCE="one_liner"
export AGENTKEY_DETECTED_AGENTS="$(detect_agents)"
export AGENTKEY_SELECTED_AGENTS="${TARGETS:-}"
export AGENTKEY_INSTALLER_FLAGS="$_flags"
export AGENTKEY_DEVICE_FINGERPRINT="$(compute_device_fingerprint "$PLATFORM")"
fi
if ! npx -y "$CLI_PACKAGE" "${AUTH_ARGS[@]}"; then
ui_error "MCP auth failed."
ui_muted "Retry manually: npx -y $CLI_PACKAGE ${AUTH_ARGS[*]}"
exit 1
fi
ui_ok "MCP server registered"
fi
# ── 4. Summary ───────────────────────────────────────────────────────
ui_step "✨ Installation complete"
cat <<EOF
${BOLD}Next steps${NC}
${MUTED}1.${NC} Restart your agent (Claude Code / Cursor / etc.)
${MUTED}2.${NC} Ask it something that needs the internet:
${ACCENT}"What has Musk been tweeting about lately?"${NC}
${BOLD}Docs${NC} https://agentkey.app/docs
${BOLD}Uninstall${NC} curl -fsSL https://agentkey.app/uninstall.sh | bash
EOF
}
main "$@"