mirror of
https://github.com/chainbase-labs/Agentkey.git
synced 2026-09-20 14:20:23 +08:00
97e3ab2fde
## What changed - teach the one-line macOS/Linux and Windows installers to detect DSH, install the AgentKey skill globally, and invoke the DSH device-code MCP writer without passing `dsh` to `skills add -a` - remove only top-level AgentKey managed blocks during uninstall while preserving marker-prefix collisions, YAML block scalars, malformed blocks, and unrelated patches - add a DSH-specific Skill setup branch explaining why DSH rc.7 must use device-code Bearer authentication instead of generic MCP OAuth - document the home-level patch, safe legacy migration, symlink-profile behavior, HMR limits, tool-policy limits, and actual readiness checks in English and Chinese - route the Kimi plugin through the client-attributed `https://api.agentkey.app/kimi/v1/mcp` alias while keeping its native OAuth flow and the same AgentKey MCP surface - add macOS/Linux Bats coverage, an attributed endpoint contract, and a Windows PowerShell marker regression gate ## Why DSH users need one global AgentKey integration across current and future profiles without selecting an AgentKey preset. DSH 0.1.0-rc.7 cannot complete the generic header-free MCP OAuth flow, so the public setup and self-repair guidance must consistently route DSH through `@agentkey/cli --auth-login --only dsh`. Kimi uses a client-attributed MCP path so Server telemetry and routing can identify the originating client without changing Kimi's OAuth or tool behavior. ## User impact After the Server and CLI dependency are released, DSH users can run the normal one-line installer or the documented two-step flow. They verify success by checking the `agentkey` Loader row and then calling `find_tools`, `describe_tool`, and `execute_tool`; Mounted/active alone is not treated as a successful MCP connection. Kimi plugin users continue to authenticate with the existing native OAuth command; only the remote path changes to the attributed alias. ## Validation - `bats tests/` (41/41) - `bash -n scripts/install.sh` - `bash -n scripts/uninstall.sh` - Windows marker regression is configured on `windows-latest` - `git diff --check` ## Release dependency - Server/CLI implementation: https://github.com/chainbase-labs/AgentKey-Server/pull/241 - deploy and verify the Server routes first - publish and canary `@agentkey/cli@1.0.4` second - release these installers, Skill changes, Kimi route, and public documentation last This is a CLI-managed DSH MCP integration, not a native installable DSH plugin. --------- Co-authored-by: Allen <0xfatdog@gmail.com>
501 lines
21 KiB
Bash
Executable File
501 lines
21 KiB
Bash
Executable File
#!/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
|
||
# DSH cleanup is marker-scoped so unrelated home/profile patches survive.
|
||
DSH_ROOT="${DSH_HOME:-}"; [[ -n "$DSH_ROOT" && "$DSH_ROOT" == *[![:space:]]* ]] || DSH_ROOT="$HOME/.dsh"
|
||
case "$DSH_ROOT" in "~"|"~/"*) DSH_ROOT="$HOME${DSH_ROOT#"~"}" ;; /*) ;; *) DSH_ROOT="$PWD/$DSH_ROOT" ;; esac
|
||
DSH_CLEANED=false
|
||
for cfg in "$DSH_ROOT/cordis.patch.yml" "$DSH_ROOT"/profiles/*/cordis.patch.yml; do
|
||
[ -f "$cfg" ] || continue; grep -qE '^# agentkey:start([[:space:]]|$)' "$cfg" 2>/dev/null || { skipped "No AgentKey DSH block in $cfg"; continue; }; tmp="${cfg}.agentkey-uninstall.$$"
|
||
if awk 'BEGIN{s=0;i=0} /^# agentkey:start([[:space:]]|$)/{if(s)i=1;s=1;next} /^# agentkey:end([[:space:]]|$)/{if(!s)i=1;else{s=0;next}} !s{print} END{if(s||i)exit 42}' "$cfg" > "$tmp"; then
|
||
grep -q '^[[:space:]]*[^#[:space:]]' "$tmp" || printf '\n[]\n' >> "$tmp"
|
||
chmod 600 "$tmp"; mv "$tmp" "$cfg"; DSH_CLEANED=true; ok "Removed AgentKey DSH block from $cfg"
|
||
else
|
||
rm -f "$tmp"; warn "Malformed AgentKey markers in $cfg — left unchanged"
|
||
fi
|
||
done
|
||
LEGACY_DSH_PRESET="$DSH_ROOT/.agent-presets/agentkey"
|
||
if [ -e "$LEGACY_DSH_PRESET" ] || [ -L "$LEGACY_DSH_PRESET" ]; then
|
||
DSH_PRESET_BACKUP="${LEGACY_DSH_PRESET}.backup-$(date -u +%Y%m%dT%H%M%SZ)"; while [ -e "$DSH_PRESET_BACKUP" ]; do DSH_PRESET_BACKUP="${DSH_PRESET_BACKUP}-1"; done; mv "$LEGACY_DSH_PRESET" "$DSH_PRESET_BACKUP"; ok "Archived legacy DSH preset at $DSH_PRESET_BACKUP"; fi
|
||
DSH_SETTINGS="$DSH_ROOT/settings.yaml"
|
||
if [ -f "$DSH_SETTINGS" ]; then
|
||
tmp="${DSH_SETTINGS}.agentkey-uninstall.$$"
|
||
if awk 'BEGIN{p=0;r=0} /^[[:space:]]*agent-presets\.default:[[:space:]]*["\047]?agentkey["\047]?[[:space:]]*(#.*)?$/{r=1;next} /^[^[:space:]#][^:]*:[[:space:]]*/{p=($0~/^agent-presets:[[:space:]]*(#.*)?$/);print;next} p&&/^[[:space:]]+default:[[:space:]]*["\047]?agentkey["\047]?[[:space:]]*(#.*)?$/{r=1;next} {print} END{if(r)exit 10}' "$DSH_SETTINGS" > "$tmp"; then status=0; else status=$?; fi
|
||
case "$status" in 0) rm -f "$tmp" ;; 10) mv "$tmp" "$DSH_SETTINGS"; ok "Removed legacy agent-presets default from $DSH_SETTINGS" ;; *) rm -f "$tmp"; warn "Could not clean legacy DSH settings in $DSH_SETTINGS" ;; esac
|
||
fi
|
||
# ── 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 ──────────────────────────────────────────────────────────────────
|
||
if $DSH_CLEANED; then
|
||
printf "\n ${BOLD}✓ Uninstall complete.${NC} Running DSH profiles watch removal; close legacy sessions or restart once if needed.\n\n"
|
||
else
|
||
printf "\n ${BOLD}✓ Uninstall complete.${NC} Restart your agent to apply changes.\n\n"
|
||
fi
|