Files
chainbase-labs__agentkey/scripts/uninstall.sh
T
不白 83363014c4 feat(installer): unify skill + MCP agent registration (16 agents) (#41)
## Summary

- Drive both `npx skills add -a` and `@agentkey/mcp --auth-login --only`
from a single detected-agent list. MCP registration now follows the same
per-host auto-detection that skill install already does, expanding MCP
auto-registration from 3 clients to **16**.
- Fix the longstanding `claude-code` marker bug: it included Claude
Desktop's config dir, causing skills CLI to target a nonexistent Claude
Code on Desktop-only machines. `claude-desktop` is now its own id
(MCP-only) — passed to `--auth-login --only` but never to `skills add`.
- Detect Claude Desktop via `/Applications/Claude.app` /
`%LOCALAPPDATA%\AnthropicClaude` so "installed but never launched" still
registers (Linux still requires the config dir).
- New `scripts/dev-smoke.sh` — sandboxed 4-phase regression suite, 42
assertions, ~10s, never touches real `$HOME`. Run before any PR touching
install/uninstall scripts.

## Depends on


[chainbase-labs/AgentKey-Server#9](https://github.com/chainbase-labs/AgentKey-Server/pull/9)
— adds `--only <ids>` to `@agentkey/mcp --auth-login`. Older CLI
versions silently ignore the flag, so this PR is forward-compatible
either way.

## Uninstaller (the bigger gap before this)

The previous uninstaller only cleaned 3 config paths and only knew the
`mcpServers.<name>` JSON shape. With 13 new agents using 4 different
schema dialects, that left AgentKey configured everywhere after
uninstall.

- Expanded MCP cleanup to **14 JSON paths + codex TOML**, covering all
16 auto-registered agents
- Schema-agnostic JSON scrub: walks the tree and drops dict keys whose
name EXACTLY matches our server names. Handles `mcpServers.<name>` /
`mcp.<name>` / `amp.mcpServers.<name>` / `projects.X.mcpServers.<name>`
in one pass
- Codex TOML splice via awk / PowerShell (no parser dep) — drops
`[mcp_servers.agentkey]` + legacy quoted block, preserves sibling
sections
- `droid mcp remove` + `openclaw mcp unset` for CLI-registered agents
- **Exact-match** server names (not substring) so user keys like
`agentkey-helper` are preserved (regression test included in dev-smoke)

## Bugs fixed during review

| Where | Bug |
|---|---|
| install.sh:487 | Unbound `$TARGETS` variable (renamed during refactor)
— `set -u` would have made this fatal |
| install.ps1 | `$SkillTargets.Count` used where `$AllTargets.Count` was
meant — diverged from install.sh behavior |
| install.sh | `--only claude-desktop` ran `skills add -a` with no
filter, defeating the user's `--only` intent. Now correctly skips the
skill step |
| install.sh helpers | Leaked-scope loop vars (`_ids`, `_id`) — declared
`local -a` |

## Test plan

- [x] `scripts/dev-smoke.sh` — 42 passing / 0 failing (Phase 1 unit
tests + Phase 2 installer + Phase 3 writer schemas + Phase 4 uninstaller
w/ false-positive guard)
- [x] `bash -n` clean for install.sh + uninstall.sh
- [x] `--list-agents` correctly lists `claude-desktop` as a separate id
- [x] `--only claude-desktop --skip-mcp --yes` walks the new "MCP-only,
skip skill" branch
- [x] Auto-detect path tested via `bash -x` trace under `set -u` (no
unbound-variable explosion)
- [x] Uninstaller decoy fixtures: `agentkey-helper`, `other-svr`,
`[mcp_servers.other]`, `[unrelated_section]` all preserved after scrub
- [ ] Windows: `install.ps1` / `uninstall.ps1` syntax-checked but not
runtime-tested (no Windows box handy — happy to test if reviewer has
one)

---------

Co-authored-by: Bruce <bruce@checkabc.me>
2026-05-20 10:07:11 +08:00

476 lines
19 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 uninstaller for macOS and Linux
# Usage: curl -fsSL https://agentkey.app/uninstall.sh | bash
# curl -fsSL https://agentkey.app/uninstall.sh | bash -s -- --keep-marketplace
# curl -fsSL https://agentkey.app/uninstall.sh | bash -s -- --force-in-repo
#
# Cleans up everything install.sh (and the legacy two-command flow) ever wrote:
# 1. Skill files in every agent (via `skills remove` — fans across 40+ agents)
# 2. MCP server entries (Claude Code / Claude Desktop / Cursor configs)
# 3. Claude Code plugin + marketplace registrations (legacy plugin install path)
# 4. Plugin / marketplace / npx caches
# 5. Shell RC exports + CLAUDE.md sections (legacy)
# 6. MCP stdio log
set -euo pipefail
KEEP_MARKETPLACE=false
FORCE_IN_REPO=false
SKIP_SKILL_REMOVE=false
for arg in "$@"; do
case "$arg" in
--keep-marketplace) KEEP_MARKETPLACE=true ;;
--force-in-repo) FORCE_IN_REPO=true ;;
--skip-skill-remove) SKIP_SKILL_REMOVE=true ;;
-h|--help)
cat <<EOF
AgentKey uninstaller (macOS / Linux)
Usage:
curl -fsSL https://agentkey.app/uninstall.sh | bash [-s -- OPTIONS]
Options:
--keep-marketplace Keep the Claude Code plugin marketplace registration
--force-in-repo Allow running inside the AgentKey-Skill source repo
--skip-skill-remove Skip 'npx skills remove' (only clean configs/caches)
-h, --help Show this help
EOF
exit 0 ;;
esac
done
# ── Colors ────────────────────────────────────────────────────────────────
# Use $'...' so variables hold real ESC bytes (works in both printf and heredoc).
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
BOLD=$'\033[1m'; SUCCESS=$'\033[38;2;0;220;150m'; INFO=$'\033[38;2;136;146;176m'
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=''; SUCCESS=''; INFO=''; WARN=''; ERROR=''; MUTED=''; NC=''
fi
info() { printf " ${INFO}${NC} %s\n" "$*"; }
ok() { printf " ${SUCCESS}${NC} %s\n" "$*"; }
warn() { printf " ${WARN}!${NC} %s\n" "$*"; }
skipped() { printf " ${MUTED}-${NC} %s\n" "$*"; }
step() { printf "\n ${BOLD}%s${NC}\n" "$*"; }
# ── Safety rail ──────────────────────────────────────────────────────────
if [ -f ".claude-plugin/plugin.json" ] \
&& grep -q '"name"[[:space:]]*:[[:space:]]*"agentkey"' .claude-plugin/plugin.json 2>/dev/null \
&& ! $FORCE_IN_REPO; then
printf "\n ${BOLD}AgentKey — Uninstall${NC}\n\n"
printf " ${ERROR}Refusing to run inside the AgentKey-Skill source repo.${NC}\n"
printf " Running here would wipe this repo's own .mcp.json and CLAUDE.md.\n"
printf " Re-run with ${BOLD}--force-in-repo${NC} if you really mean it.\n\n"
exit 2
fi
printf "\n ${BOLD}AgentKey — Uninstall${NC}\n"
printf " ${MUTED}https://agentkey.app${NC}\n"
# ── 1. Remove the skill via skills CLI ────────────────────────────────────
step "1. Skill files"
if $SKIP_SKILL_REMOVE; then
skipped "Skipped (--skip-skill-remove)"
elif ! command -v npx >/dev/null 2>&1; then
warn "npx not found — skipping 'skills remove' (manual: npx skills remove agentkey -g)"
else
# `skills remove` takes the **skill name** (`agentkey`), not the repo path.
# The CLI also exits 0 when nothing matches, so we inspect stdout instead.
info "Running: npx -y skills remove agentkey -g -y"
REMOVE_OUTPUT="$(npx -y skills remove agentkey -g -y 2>&1 || true)"
if printf '%s\n' "$REMOVE_OUTPUT" | grep -q "Successfully removed"; then
ok "Skill removed from detected agents"
elif printf '%s\n' "$REMOVE_OUTPUT" | grep -q "No matching skills found"; then
skipped "Not registered with 'skills' CLI (already removed or installed via plugin marketplace)"
else
warn "'skills remove' produced unexpected output — some agents may still have skill files"
warn "Check manually: npx skills list -g"
fi
fi
# ── 2. MCP config cleanup ────────────────────────────────────────────────
step "2. MCP server entries"
OS="$(uname -s)"
# All known JSON MCP config paths across the 16 auto-supported agents. The
# scrub logic is intentionally schema-agnostic — it walks the JSON tree and
# drops any key named "agentkey" / "agentkey.app AgentKey" wherever it
# finds it, so the same scrubber handles:
# - mcpServers.agentkey (Claude Code, Desktop, Cursor, Gemini, Windsurf, Warp, Qwen, iFlow, Kimi, Kiro)
# - projects.*.mcpServers (Claude Code's per-project shape)
# - mcp.agentkey (OpenCode, Crush)
# - amp.mcpServers.agentkey (Amp's flat dotted key)
# Keep this list in sync with AGENT_REGISTRY in
# AgentKey-Server/cli/src/lib/mcp-clients.ts.
MCP_JSON_CONFIGS=(
"$HOME/.claude.json" # Claude Code
"$HOME/.cursor/mcp.json" # Cursor
"$HOME/.gemini/settings.json" # Gemini CLI
"$HOME/.qwen/settings.json" # Qwen Code
"$HOME/.iflow/settings.json" # iFlow CLI
"$HOME/.kimi/mcp.json" # Kimi CLI
"$HOME/.kiro/settings/mcp.json" # Kiro CLI
"$HOME/.codeium/windsurf/mcp_config.json" # Windsurf
"$HOME/.warp/.mcp.json" # Warp
"$HOME/.config/opencode/opencode.json" # OpenCode (mcp.<name>)
"$HOME/.config/amp/settings.json" # Amp (amp.mcpServers.<name>)
"$HOME/.config/crush/crush.json" # Crush (mcp.<name>)
)
if [ "$OS" = "Darwin" ]; then
MCP_JSON_CONFIGS+=("$HOME/Library/Application Support/Claude/claude_desktop_config.json")
else
MCP_JSON_CONFIGS+=("$HOME/.config/Claude/claude_desktop_config.json")
fi
# TOML configs — handled separately because we can't parse TOML without a
# library. We splice out `[mcp_servers.agentkey]` blocks via line-scan.
MCP_TOML_CONFIGS=(
"$HOME/.codex/config.toml" # Codex CLI
)
have_python() { command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; }
py() { if command -v python3 >/dev/null 2>&1; then python3 "$@"; else python "$@"; fi; }
if ! have_python; then
warn "python not found — skipping JSON cleanup; edit these files manually:"
for f in "${MCP_JSON_CONFIGS[@]}"; do [ -f "$f" ] && echo " $f"; done
else
for cfg in "${MCP_JSON_CONFIGS[@]}"; do
if [ ! -f "$cfg" ]; then
skipped "$(basename "$cfg") not found"
continue
fi
RESULT=$(py - "$cfg" <<'EOF'
import json, sys
path = sys.argv[1]
try:
with open(path) as f: d = json.load(f)
except Exception as e:
print(f"ERROR: {e}"); sys.exit(0)
# Schema-agnostic recursive scrub: drop any dict key whose name exactly
# matches one of our known MCP server names (current + legacy). Covers every
# dialect we write: mcpServers.agentkey, mcp.agentkey,
# amp.mcpServers.agentkey, projects.X.mcpServers.agentkey, etc.
# Exact-match (not substring) so we don't nuke unrelated user keys like
# "agentkey-helper" or "my-agentkey-config".
NAMES = {'agentkey', 'agentkey.app agentkey'}
def scrub(obj):
removed = 0
if isinstance(obj, dict):
for k in list(obj.keys()):
if k.lower() in NAMES:
del obj[k]; removed += 1
else:
removed += scrub(obj[k])
elif isinstance(obj, list):
for item in obj:
removed += scrub(item)
return removed
removed = scrub(d)
if removed:
with open(path, 'w') as f:
json.dump(d, f, indent=2)
print(removed)
EOF
)
if [ "$RESULT" = "0" ]; then
skipped "No agentkey entry in $cfg"
elif [[ "$RESULT" =~ ^[0-9]+$ ]]; then
ok "Removed $RESULT entry/entries from $cfg"
else
warn "Failed to update $cfg: $RESULT"
fi
done
fi
# Codex TOML: splice out [mcp_servers.agentkey] blocks (bare + legacy quoted).
# Bash-only — no Python dependency — because we never parse, only line-edit.
for cfg in "${MCP_TOML_CONFIGS[@]}"; do
if [ ! -f "$cfg" ]; then
skipped "$(basename "$cfg") not found"
continue
fi
if ! grep -qE '^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")\s*\]' "$cfg" 2>/dev/null; then
skipped "No agentkey block in $cfg"
continue
fi
# Line-scan: drop from our header line to the next [section] header or EOF.
awk '
BEGIN { skip = 0 }
/^[[:space:]]*\[[[:space:]]*mcp_servers[[:space:]]*\.[[:space:]]*(agentkey|"agentkey\.app AgentKey")[[:space:]]*\][[:space:]]*$/ { skip = 1; next }
/^[[:space:]]*\[[^]]+\][[:space:]]*$/ { skip = 0 }
!skip { print }
' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg"
ok "Removed agentkey block from $cfg"
done
# ── 2b. CLI-registered agents (droid / openclaw) ─────────────────────────
# These two agents have no documented file-edit path; we registered them via
# their own CLIs (`droid mcp add`, `openclaw mcp set`), so we have to use the
# CLI to unregister. Best-effort — silently skip if the CLI isn't on PATH or
# the entry was never created.
step "2b. CLI-registered agents (droid / openclaw)"
if command -v droid >/dev/null 2>&1; then
if droid mcp remove agentkey >/dev/null 2>&1; then
ok "Removed agentkey from droid (\`droid mcp remove\`)"
else
skipped "No agentkey entry in droid (or already removed)"
fi
else
skipped "droid CLI not on PATH"
fi
if command -v openclaw >/dev/null 2>&1; then
if openclaw mcp unset agentkey >/dev/null 2>&1; then
ok "Removed agentkey from openclaw (\`openclaw mcp unset\`)"
else
skipped "No agentkey entry in openclaw (or already removed)"
fi
else
skipped "openclaw CLI not on PATH"
fi
# ── 3. Claude Code plugin registrations (legacy) ─────────────────────────
step "3. Claude Code plugin registrations (legacy)"
if ! command -v claude >/dev/null 2>&1; then
skipped "Claude Code CLI not on PATH — nothing to do here"
else
PLUGIN_LIST=$(claude plugin list 2>/dev/null || true)
MARKETS=$(echo "$PLUGIN_LIST" | grep -oE 'agentkey@[a-zA-Z0-9_-]+' || true)
if [ -z "$MARKETS" ]; then
skipped "No agentkey plugin registered"
else
while IFS= read -r entry; do
name="${entry%%@*}"
info "Uninstalling $entry ..."
if claude plugin uninstall "$name" --scope user 2>/dev/null; then
ok "Removed $entry"
else
warn "Could not remove $entry — try: claude plugin uninstall $name --scope user"
fi
done <<< "$MARKETS"
fi
# Legacy MCP registration via `claude mcp`
if claude mcp list 2>/dev/null | grep -q "^agentkey"; then
info "Removing MCP server 'agentkey' via claude CLI ..."
if claude mcp remove agentkey 2>/dev/null; then
ok "MCP server removed"
else
warn "Could not remove MCP server via claude CLI"
fi
else
skipped "No 'agentkey' MCP via claude CLI"
fi
# Marketplace entry
if $KEEP_MARKETPLACE; then
skipped "Marketplace removal skipped (--keep-marketplace)"
else
AGENTKEY_MARKETS=$(claude plugin marketplace list 2>/dev/null \
| grep -B1 -A1 -E "(AgentKey-Skill|chainbase-labs/AgentKey-Skill|agentkey-skill|chainbase-labs/agentkey)" \
| grep -oE '^ [a-zA-Z0-9_-]+' | awk '{print $2}' || true)
if [ -z "$AGENTKEY_MARKETS" ]; then
skipped "No AgentKey marketplace entry"
else
while IFS= read -r mkt; do
info "Removing marketplace '$mkt' ..."
if claude plugin marketplace remove "$mkt" 2>/dev/null; then
ok "Removed marketplace '$mkt'"
else
warn "Could not remove marketplace '$mkt'"
fi
done <<< "$AGENTKEY_MARKETS"
fi
fi
fi
# ── 4. Plugin + marketplace caches ────────────────────────────────────────
step "4. Plugin / marketplace caches"
CACHE_HITS=()
for d in "$HOME/.claude/plugins/cache"/agentkey \
"$HOME/.claude/plugins/cache"/agentkey-skill \
"$HOME/.claude/plugins/cache"/agentkey-*; do
[ -d "$d" ] && CACHE_HITS+=("$d")
done
if [ -d "$HOME/.claude/plugins/marketplaces" ]; then
for d in "$HOME/.claude/plugins/marketplaces"/*agentkey*; do
[ -d "$d" ] && CACHE_HITS+=("$d")
done
fi
if [ ${#CACHE_HITS[@]} -eq 0 ]; then
skipped "No cache found"
else
for d in "${CACHE_HITS[@]}"; do
rm -rf "$d" && ok "Removed $d"
done
fi
# ── 5. Shell RC environment exports (legacy) ──────────────────────────────
step "5. Shell environment exports (legacy)"
changed=false
for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
[ -f "$RC" ] || continue
if grep -q "agentkey-env-start" "$RC" 2>/dev/null; then
if have_python; then
py - "$RC" <<'EOF'
import re, sys
p = sys.argv[1]
c = open(p).read()
n = re.sub(r'\n# agentkey-env-start.*?# agentkey-env-end', '', c, flags=re.DOTALL)
if n != c: open(p, 'w').write(n)
EOF
ok "Cleaned agentkey env block from $RC"
changed=true
else
warn "python not found — edit $RC manually (look for 'agentkey-env-start')"
fi
fi
done
$changed || skipped "No agentkey env block in any shell RC"
# ── 6. CLAUDE.md cleanup (legacy) ─────────────────────────────────────────
step "6. CLAUDE.md sections (legacy)"
md_changed=false
for CLAUDE_MD in "$HOME/.claude/CLAUDE.md" ".claude/CLAUDE.md" "CLAUDE.md"; do
[ -f "$CLAUDE_MD" ] || continue
grep -q "AgentKey\|agentkey\|AGENTKEY" "$CLAUDE_MD" 2>/dev/null || continue
if have_python; then
OUT=$(py - "$CLAUDE_MD" <<'EOF'
import re, sys
p = sys.argv[1]
c = open(p).read()
c2 = re.sub(r'\n# AgentKey\n.*?(?=\n# |\Z)', '', c, flags=re.DOTALL)
c2 = re.sub(r'\n[^\n]*(\.agentkey|agentkey.*activation\.md|agentkey.*SKILL\.md)[^\n]*', '', c2, flags=re.IGNORECASE)
if c2 != c:
open(p, 'w').write(c2)
print("CHANGED")
else:
print("NO_MATCH")
EOF
)
if [ "$OUT" = "CHANGED" ]; then
ok "Removed AgentKey section from $CLAUDE_MD"
md_changed=true
fi
else
warn "python not found — edit $CLAUDE_MD manually"
fi
done
$md_changed || skipped "No removable AgentKey section in CLAUDE.md"
# ── 7. npm / npx caches ───────────────────────────────────────────────────
step "7. npm / npx caches"
if command -v npm >/dev/null 2>&1; then
_global_list="$(npm list -g --depth=0 2>/dev/null || true)"
_removed_any=false
# Sweep both the current package name and the legacy v0.x name so
# users who installed before the @agentkey/mcp → @agentkey/cli rename
# get a clean uninstall.
for _pkg in "@agentkey/cli" "@agentkey/mcp"; do
if echo "$_global_list" | grep -q "$_pkg"; then
info "Uninstalling global $_pkg ..."
if npm uninstall -g "$_pkg" >/dev/null 2>&1; then
ok "Removed $_pkg"
_removed_any=true
else
warn "Could not remove $_pkg — try: npm uninstall -g $_pkg"
fi
fi
done
if ! $_removed_any; then
skipped "No global @agentkey/cli or @agentkey/mcp installed"
fi
else
skipped "npm not on PATH"
fi
if [ -d "$HOME/.npm/_npx" ]; then
NPX_HITS=$(find "$HOME/.npm/_npx" -maxdepth 3 -type d -iname "*agentkey*" 2>/dev/null || true)
if [ -n "$NPX_HITS" ]; then
echo "$NPX_HITS" | xargs rm -rf
ok "Cleared agentkey entries from npx cache"
else
skipped "No agentkey entries in npx cache"
fi
else
skipped "No ~/.npm/_npx directory"
fi
# ── 7b. AgentKey config dir (snooze/disable/telemetry state) ─────────────
step "7b. AgentKey config directory"
AGENTKEY_CFG="$HOME/.config/agentkey"
if [ -d "$AGENTKEY_CFG" ]; then
rm -rf "$AGENTKEY_CFG" && ok "Removed $AGENTKEY_CFG"
else
skipped "No $AGENTKEY_CFG directory"
fi
# ── 8. Residual artifacts ─────────────────────────────────────────────────
step "8. Residual artifacts"
# 8a. MCP stdio log (macOS only; Claude Desktop path)
MCP_LOG_MAC="$HOME/Library/Logs/Claude/mcp-server-agentkey.log"
if [ -f "$MCP_LOG_MAC" ]; then
rm -f "$MCP_LOG_MAC" && ok "Removed $MCP_LOG_MAC"
fi
# 8b. Plugin registry JSONs — strip any agentkey-keyed entries
if have_python; then
for REG in "$HOME/.claude/plugins/installed_plugins.json" \
"$HOME/.claude/plugins/known_marketplaces.json" \
"$HOME/.claude/mcp-needs-auth-cache.json"; do
[ -f "$REG" ] || continue
RESULT=$(py - "$REG" <<'EOF'
import json, sys
p = sys.argv[1]
try:
with open(p) as f: d = json.load(f)
except Exception: sys.exit(0)
def scrub(obj):
removed = 0
if isinstance(obj, dict):
for k in list(obj.keys()):
if 'agentkey' in k.lower(): del obj[k]; removed += 1
else: removed += scrub(obj[k])
elif isinstance(obj, list):
kept = []
for item in obj:
s = json.dumps(item).lower() if not isinstance(item, str) else item.lower()
if 'agentkey' in s: removed += 1
else: removed += scrub(item); kept.append(item)
obj[:] = kept
return removed
n = scrub(d)
if n:
with open(p, 'w') as f: json.dump(d, f, indent=2)
print(n)
EOF
)
if [[ "$RESULT" =~ ^[0-9]+$ ]] && [ "$RESULT" -gt 0 ]; then
ok "Cleaned $RESULT entry/entries from $REG"
fi
done
fi
# ── Done ──────────────────────────────────────────────────────────────────
printf "\n ${BOLD}✓ Uninstall complete.${NC} Restart your agent to apply changes.\n\n"