Files
chainbase-labs__agentkey/scripts/uninstall.ps1
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

423 lines
18 KiB
PowerShell
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.
#Requires -Version 5.1
<#
.SYNOPSIS
AgentKey uninstaller for Windows
.DESCRIPTION
Usage:
irm https://agentkey.app/uninstall.ps1 | iex
& ([scriptblock]::Create((irm https://agentkey.app/uninstall.ps1))) -KeepMarketplace
& ([scriptblock]::Create((irm https://agentkey.app/uninstall.ps1))) -ForceInRepo
Cleans up everything install.ps1 (and the legacy two-command flow) ever wrote:
1. Skill files in every agent (via `skills remove`)
2. MCP server entries (Claude Code / Claude Desktop / Cursor)
3. Plugin + marketplace caches
4. CLAUDE.md sections + npm/npx caches (legacy)
#>
[CmdletBinding()]
param(
[switch]$KeepMarketplace,
[switch]$ForceInRepo,
[switch]$SkipSkillRemove,
[switch]$Help
)
$ErrorActionPreference = 'Continue'
if ($Help) {
@'
AgentKey uninstaller (Windows)
Usage:
irm https://agentkey.app/uninstall.ps1 | iex
Parameters:
-KeepMarketplace Keep the Claude Code plugin marketplace registration
-ForceInRepo Allow running inside the AgentKey-Skill source repo
-SkipSkillRemove Skip 'npx skills remove' (only clean configs/caches)
-Help Show this help
'@
exit 0
}
# ── UI helpers ────────────────────────────────────────────────────────────
function Write-Step ($t) { Write-Host ''; Write-Host " $t" -ForegroundColor White }
function Write-Info ($t) { Write-Host " $t" -ForegroundColor Gray }
function Write-Ok ($t) { Write-Host "$t" -ForegroundColor Green }
function Write-Warn2($t) { Write-Host " ! $t" -ForegroundColor Yellow }
function Write-Skip ($t) { Write-Host " - $t" -ForegroundColor DarkGray }
function Write-Err ($t) { Write-Host "$t" -ForegroundColor Red }
# ── Safety rail ──────────────────────────────────────────────────────────
if ((Test-Path '.claude-plugin/plugin.json') -and -not $ForceInRepo) {
$content = Get-Content '.claude-plugin/plugin.json' -Raw -ErrorAction SilentlyContinue
if ($content -match '"name"\s*:\s*"agentkey"') {
Write-Host ''
Write-Host ' AgentKey — Uninstall' -ForegroundColor White
Write-Host ''
Write-Err 'Refusing to run inside the AgentKey-Skill source repo.'
Write-Host " Running here would wipe this repo's own .mcp.json and CLAUDE.md." -ForegroundColor DarkGray
Write-Host ' Re-run with -ForceInRepo if you really mean it.' -ForegroundColor DarkGray
Write-Host ''
exit 2
}
}
Write-Host ''
Write-Host ' AgentKey — Uninstall' -ForegroundColor White
Write-Host ' https://agentkey.app' -ForegroundColor DarkGray
# ── 1. Skill removal via skills CLI ──────────────────────────────────────
Write-Step '1. Skill files'
if ($SkipSkillRemove) {
Write-Skip 'Skipped (-SkipSkillRemove)'
} elseif (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
Write-Warn2 "npx not found — skipping 'skills remove'"
Write-Host ' Manual: npx skills remove agentkey -g' -ForegroundColor DarkGray
} 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.
Write-Info 'Running: npx -y skills remove agentkey -g -y'
$removeOutput = (& npx -y skills remove agentkey -g -y 2>&1) -join "`n"
if ($removeOutput -match 'Successfully removed') {
Write-Ok 'Skill removed from detected agents'
} elseif ($removeOutput -match 'No matching skills found') {
Write-Skip "Not registered with 'skills' CLI (already removed or installed via plugin marketplace)"
} else {
Write-Warn2 "'skills remove' produced unexpected output — some agents may still have skill files"
Write-Host ' Check manually: npx skills list -g' -ForegroundColor DarkGray
}
}
# ── 2. MCP config cleanup ────────────────────────────────────────────────
Write-Step '2. MCP server entries'
$home2 = [Environment]::GetFolderPath('UserProfile')
# All known JSON MCP config paths across the 16 auto-supported agents. The
# scrub logic is schema-agnostic — it walks the JSON tree and drops any
# dict key whose name exactly matches our server name (current + legacy),
# so the same scrubber handles every dialect: mcpServers, mcp,
# amp.mcpServers, projects.X.mcpServers, etc. Keep this list aligned with
# AGENT_REGISTRY in AgentKey-Server/cli/src/lib/mcp-clients.ts.
$mcpJsonConfigs = @(
(Join-Path $home2 '.claude.json'), # Claude Code
(Join-Path $home2 '.cursor\mcp.json'), # Cursor
(Join-Path $env:APPDATA 'Claude\claude_desktop_config.json'), # Claude Desktop
(Join-Path $home2 '.gemini\settings.json'), # Gemini CLI
(Join-Path $home2 '.qwen\settings.json'), # Qwen Code
(Join-Path $home2 '.iflow\settings.json'), # iFlow CLI
(Join-Path $home2 '.kimi\mcp.json'), # Kimi CLI
(Join-Path $home2 '.kiro\settings\mcp.json'), # Kiro CLI
(Join-Path $home2 '.codeium\windsurf\mcp_config.json'), # Windsurf
(Join-Path $home2 '.warp\.mcp.json'), # Warp
(Join-Path $env:APPDATA 'opencode\opencode.json'), # OpenCode (mcp.<name>)
(Join-Path $env:APPDATA 'amp\settings.json'), # Amp (amp.mcpServers.<name>)
(Join-Path $env:APPDATA 'crush\crush.json') # Crush (mcp.<name>)
)
# TOML config — handled separately (no built-in TOML parser pre-PS7.4).
$mcpTomlConfigs = @(
(Join-Path $home2 '.codex\config.toml') # Codex CLI
)
$ServerNames = @('agentkey', 'agentkey.app agentkey')
# Schema-agnostic recursive scrub: drop any dict key whose lowercased name
# is in $ServerNames. Covers mcpServers / mcp / amp.mcpServers / projects.*
# in one pass. Exact match (not substring) so we don't nuke unrelated user
# keys like "my-agentkey-helper".
function Scrub-Mcp($node) {
$removed = 0
if ($node -is [System.Management.Automation.PSCustomObject]) {
$keys = @($node.PSObject.Properties.Name)
foreach ($k in $keys) {
if ($ServerNames -contains $k.ToLower()) {
$node.PSObject.Properties.Remove($k)
$removed++
} else {
$removed += Scrub-Mcp $node.$k
}
}
} elseif ($node -is [System.Collections.IList]) {
foreach ($item in $node) {
$removed += Scrub-Mcp $item
}
}
return $removed
}
function Clean-McpJsonConfig($path) {
if (-not (Test-Path $path)) {
Write-Skip "$([System.IO.Path]::GetFileName($path)) not found"
return
}
try {
$raw = Get-Content $path -Raw
$obj = $raw | ConvertFrom-Json
} catch {
Write-Warn2 "Could not parse $path — skipping"
return
}
$removed = Scrub-Mcp $obj
if ($removed -gt 0) {
($obj | ConvertTo-Json -Depth 100) | Set-Content -Path $path -Encoding UTF8
Write-Ok "Removed $removed entry/entries from $path"
} else {
Write-Skip "No agentkey entry in $path"
}
}
foreach ($cfg in $mcpJsonConfigs) { Clean-McpJsonConfig $cfg }
# Codex TOML — line-scan to splice out our [mcp_servers.agentkey] block
# (bare + legacy quoted name). Done in pure PowerShell — no TOML lib needed
# because we only ever delete, never re-emit unrelated tables.
function Clean-McpTomlConfig($path) {
if (-not (Test-Path $path)) {
Write-Skip "$([System.IO.Path]::GetFileName($path)) not found"
return
}
$lines = Get-Content $path
$headerPattern = '^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")\s*\]\s*$'
$anyHeader = '^\s*\[[^\]]+\]\s*$'
if (-not ($lines -match $headerPattern)) {
Write-Skip "No agentkey block in $path"
return
}
$out = New-Object System.Collections.Generic.List[string]
$skip = $false
foreach ($line in $lines) {
if ($line -match $headerPattern) { $skip = $true; continue }
if ($skip -and $line -match $anyHeader) { $skip = $false }
if (-not $skip) { $out.Add($line) }
}
Set-Content -Path $path -Value $out -Encoding UTF8
Write-Ok "Removed agentkey block from $path"
}
foreach ($cfg in $mcpTomlConfigs) { Clean-McpTomlConfig $cfg }
# ── 2b. CLI-registered agents (droid / openclaw) ─────────────────────────
# These two have no documented file-edit path; we registered them via their
# CLIs so we have to unregister the same way. Best-effort — silently skip
# if the CLI isn't on PATH or the entry was never created.
Write-Step '2b. CLI-registered agents (droid / openclaw)'
if (Get-Command droid -ErrorAction SilentlyContinue) {
& droid mcp remove agentkey 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { Write-Ok 'Removed agentkey from droid (`droid mcp remove`)' }
else { Write-Skip 'No agentkey entry in droid (or already removed)' }
} else {
Write-Skip 'droid CLI not on PATH'
}
if (Get-Command openclaw -ErrorAction SilentlyContinue) {
& openclaw mcp unset agentkey 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { Write-Ok 'Removed agentkey from openclaw (`openclaw mcp unset`)' }
else { Write-Skip 'No agentkey entry in openclaw (or already removed)' }
} else {
Write-Skip 'openclaw CLI not on PATH'
}
# ── 3. Claude Code plugin registrations (legacy) ─────────────────────────
Write-Step '3. Claude Code plugin registrations (legacy)'
if (-not (Get-Command claude -ErrorAction SilentlyContinue)) {
Write-Skip 'Claude Code CLI not on PATH — nothing to do here'
} else {
$pluginList = & claude plugin list 2>$null
$markets = @()
if ($pluginList) {
$markets = $pluginList | Select-String -Pattern 'agentkey@[a-zA-Z0-9_-]+' -AllMatches |
ForEach-Object { $_.Matches.Value } | Sort-Object -Unique
}
if ($markets.Count -eq 0) {
Write-Skip 'No agentkey plugin registered'
} else {
foreach ($entry in $markets) {
$name = $entry.Split('@')[0]
Write-Info "Uninstalling $entry ..."
& claude plugin uninstall $name --scope user 2>$null
if ($LASTEXITCODE -eq 0) { Write-Ok "Removed $entry" }
else { Write-Warn2 "Could not remove $entry" }
}
}
$mcpList = & claude mcp list 2>$null
if ($mcpList -and ($mcpList -match '^agentkey')) {
Write-Info "Removing MCP server 'agentkey' via claude CLI ..."
& claude mcp remove agentkey 2>$null
if ($LASTEXITCODE -eq 0) { Write-Ok 'MCP server removed' }
else { Write-Warn2 'Could not remove MCP server via claude CLI' }
} else {
Write-Skip "No 'agentkey' MCP via claude CLI"
}
if ($KeepMarketplace) {
Write-Skip 'Marketplace removal skipped (-KeepMarketplace)'
} else {
$mktList = & claude plugin marketplace list 2>$null
$agentkeyMkts = @()
if ($mktList) {
$joined = $mktList -join "`n"
if ($joined -match '(AgentKey-Skill|chainbase-labs/AgentKey-Skill|agentkey-skill|chainbase-labs/agentkey)') {
$agentkeyMkts = $mktList | Select-String -Pattern '^\s*\s+([a-zA-Z0-9_-]+)' |
ForEach-Object { $_.Matches[0].Groups[1].Value }
}
}
if ($agentkeyMkts.Count -eq 0) {
Write-Skip 'No AgentKey marketplace entry'
} else {
foreach ($mkt in $agentkeyMkts) {
Write-Info "Removing marketplace '$mkt' ..."
& claude plugin marketplace remove $mkt 2>$null
if ($LASTEXITCODE -eq 0) { Write-Ok "Removed marketplace '$mkt'" }
else { Write-Warn2 "Could not remove marketplace '$mkt'" }
}
}
}
}
# ── 4. Plugin + marketplace caches ────────────────────────────────────────
Write-Step '4. Plugin / marketplace caches'
$cacheHits = @()
$pluginCache = Join-Path $home2 '.claude\plugins\cache'
if (Test-Path $pluginCache) {
$cacheHits += Get-ChildItem -Path $pluginCache -Directory -Filter 'agentkey*' -ErrorAction SilentlyContinue
}
$mktCache = Join-Path $home2 '.claude\plugins\marketplaces'
if (Test-Path $mktCache) {
$cacheHits += Get-ChildItem -Path $mktCache -Directory -Filter '*agentkey*' -ErrorAction SilentlyContinue
}
if ($cacheHits.Count -eq 0) {
Write-Skip 'No cache found'
} else {
foreach ($d in $cacheHits) {
Remove-Item -Recurse -Force $d.FullName -ErrorAction SilentlyContinue
Write-Ok "Removed $($d.FullName)"
}
}
# ── 5. CLAUDE.md cleanup (legacy) ─────────────────────────────────────────
Write-Step '5. CLAUDE.md sections (legacy)'
$mdChanged = $false
$candidates = @(
(Join-Path $home2 '.claude\CLAUDE.md'),
'.claude\CLAUDE.md',
'CLAUDE.md'
)
foreach ($md in $candidates) {
if (-not (Test-Path $md)) { continue }
$c = Get-Content $md -Raw
if ($c -notmatch '(AgentKey|agentkey|AGENTKEY)') { continue }
$c2 = [regex]::Replace($c, '\n# AgentKey\n.*?(?=\n# |\z)', '', 'Singleline')
$c2 = [regex]::Replace($c2, '\n[^\n]*(\.agentkey|agentkey.*activation\.md|agentkey.*SKILL\.md)[^\n]*', '', 'IgnoreCase')
if ($c2 -ne $c) {
Set-Content -Path $md -Value $c2 -Encoding UTF8
Write-Ok "Removed AgentKey section from $md"
$mdChanged = $true
}
}
if (-not $mdChanged) { Write-Skip 'No removable AgentKey section in CLAUDE.md' }
# ── 6. npm / npx caches ───────────────────────────────────────────────────
Write-Step '6. npm / npx caches'
if (Get-Command npm -ErrorAction SilentlyContinue) {
$globalList = & npm list -g --depth=0 2>$null
$removedAny = $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.
foreach ($pkg in @('@agentkey/cli', '@agentkey/mcp')) {
if ($globalList -and ($globalList -match [regex]::Escape($pkg))) {
Write-Info "Uninstalling global $pkg ..."
& npm uninstall -g $pkg 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { Write-Ok "Removed $pkg"; $removedAny = $true }
else { Write-Warn2 "Could not remove $pkg" }
}
}
if (-not $removedAny) {
Write-Skip 'No global @agentkey/cli or @agentkey/mcp installed'
}
} else {
Write-Skip 'npm not on PATH'
}
$npxCache = Join-Path $home2 '.npm\_npx'
if (Test-Path $npxCache) {
$hits = Get-ChildItem -Path $npxCache -Directory -Recurse -Depth 2 -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match 'agentkey' }
if ($hits) {
$hits | ForEach-Object { Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue }
Write-Ok 'Cleared agentkey entries from npx cache'
} else {
Write-Skip 'No agentkey entries in npx cache'
}
} else {
Write-Skip 'No npx cache directory'
}
# ── 7. Residual plugin registries ─────────────────────────────────────────
Write-Step '7. Residual plugin registries'
$regs = @(
(Join-Path $home2 '.claude\plugins\installed_plugins.json'),
(Join-Path $home2 '.claude\plugins\known_marketplaces.json'),
(Join-Path $home2 '.claude\mcp-needs-auth-cache.json')
)
function Scrub-Agentkey($node) {
$removed = 0
if ($node -is [System.Management.Automation.PSCustomObject]) {
$keys = @($node.PSObject.Properties.Name)
foreach ($k in $keys) {
if ($k -match 'agentkey') {
$node.PSObject.Properties.Remove($k)
$removed++
} else {
$removed += Scrub-Agentkey $node.$k
}
}
} elseif ($node -is [System.Collections.IList]) {
# Rebuild list without agentkey items
$kept = New-Object System.Collections.ArrayList
foreach ($item in $node) {
$s = ($item | ConvertTo-Json -Depth 10 -Compress).ToLower()
if ($s -match 'agentkey') {
$removed++
} else {
$removed += Scrub-Agentkey $item
[void]$kept.Add($item)
}
}
return @{ removed = $removed; list = $kept }
}
return $removed
}
foreach ($reg in $regs) {
if (-not (Test-Path $reg)) { continue }
try {
$obj = Get-Content $reg -Raw | ConvertFrom-Json
} catch { continue }
$res = Scrub-Agentkey $obj
$n = if ($res -is [hashtable]) { $res.removed } else { $res }
if ($n -gt 0) {
($obj | ConvertTo-Json -Depth 100) | Set-Content -Path $reg -Encoding UTF8
Write-Ok "Cleaned $n entry/entries from $reg"
}
}
# ── Done ──────────────────────────────────────────────────────────────────
Write-Host ''
Write-Host ' ✓ Uninstall complete.' -ForegroundColor Green -NoNewline
Write-Host ' Restart your agent to apply changes.' -ForegroundColor White
Write-Host ''