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
22 KiB
PowerShell
501 lines
22 KiB
PowerShell
#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 }
|
||
# DSH stores the MCP loader in the home patch; old installs used profiles.
|
||
$DshHome = if ([string]::IsNullOrWhiteSpace($env:DSH_HOME)) { Join-Path $home2 '.dsh' } else { $env:DSH_HOME }
|
||
if ($DshHome -eq '~') { $DshHome = $home2 }
|
||
elseif ($DshHome -match '^~[\\/]') { $DshHome = Join-Path $home2 $DshHome.Substring(2) }
|
||
$DshHome = [System.IO.Path]::GetFullPath($DshHome)
|
||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||
$managedPattern = '(?ms)^# agentkey:start(?:[ \t][^\r\n]*)?(?:\r?\n|$).*?^# agentkey:end(?:[ \t][^\r\n]*)?(?:\r?\n|$)'
|
||
$profilesDir = Join-Path $DshHome 'profiles'
|
||
$dshCleaned = $false
|
||
$dshPatches = New-Object System.Collections.Generic.List[string]
|
||
$homePatch = Join-Path $DshHome 'cordis.patch.yml'; if (Test-Path $homePatch) { [void]$dshPatches.Add($homePatch) }
|
||
if (Test-Path $profilesDir) {
|
||
foreach ($profile in Get-ChildItem $profilesDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'node_modules' }) {
|
||
$patch = Join-Path $profile.FullName 'cordis.patch.yml'
|
||
if (Test-Path $patch) { [void]$dshPatches.Add($patch) }
|
||
}
|
||
}
|
||
foreach ($patch in $dshPatches) {
|
||
$raw = [System.IO.File]::ReadAllText($patch)
|
||
$markerDepth = 0
|
||
$malformedMarkers = $false
|
||
foreach ($line in ($raw -split '\r?\n')) {
|
||
if ($line -match '^# agentkey:start(?:[ \t]|$)') {
|
||
if ($markerDepth -ne 0) { $malformedMarkers = $true }; $markerDepth++
|
||
} elseif ($line -match '^# agentkey:end(?:[ \t]|$)') {
|
||
if ($markerDepth -ne 1) { $malformedMarkers = $true }; if ($markerDepth -gt 0) { $markerDepth-- }
|
||
}
|
||
}
|
||
if ($markerDepth -ne 0) { $malformedMarkers = $true }
|
||
if ($malformedMarkers) {
|
||
Write-Warn2 "Malformed AgentKey markers in $patch — left unchanged"
|
||
continue
|
||
}
|
||
$updated = [regex]::Replace($raw, $managedPattern, '')
|
||
if ($updated -ne $raw) {
|
||
$meaningful = @($updated -split '\r?\n' | Where-Object { $_ -match '^\s*[^#\s]' })
|
||
if ($meaningful.Count -eq 0) {
|
||
$eol = if ($raw.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||
$prefix = $updated.TrimEnd()
|
||
$updated = if ($prefix) { $prefix + $eol + $eol + '[]' + $eol } else { '[]' + $eol }
|
||
}
|
||
[System.IO.File]::WriteAllText($patch, $updated, $utf8NoBom)
|
||
$dshCleaned = $true
|
||
Write-Ok "Removed AgentKey DSH block from $patch"
|
||
} elseif ($raw -match '(?m)^# agentkey:start(?:[ \t]|$)') {
|
||
Write-Warn2 "Malformed AgentKey markers in $patch — left unchanged"
|
||
} else {
|
||
Write-Skip "No AgentKey DSH block in $patch"
|
||
}
|
||
}
|
||
$legacyDshPreset = Join-Path $DshHome '.agent-presets\agentkey'
|
||
if (Test-Path $legacyDshPreset) {
|
||
$presetBackup = "$legacyDshPreset.backup-$([DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ'))"; while (Test-Path $presetBackup) { $presetBackup += '-1' }
|
||
Move-Item $legacyDshPreset $presetBackup; Write-Ok "Archived legacy DSH preset at $presetBackup" }
|
||
$dshSettings = Join-Path $DshHome 'settings.yaml'
|
||
if (Test-Path $dshSettings) {
|
||
$lines = [System.IO.File]::ReadAllLines($dshSettings)
|
||
$out = New-Object System.Collections.Generic.List[string]
|
||
$inPresets = $false
|
||
$removedDefault = $false
|
||
foreach ($line in $lines) {
|
||
if ($line -match '^\s*agent-presets\.default:\s*["'']?agentkey["'']?\s*(?:#.*)?$') {
|
||
$removedDefault = $true
|
||
continue
|
||
}
|
||
if ($line -match '^[^\s#][^:]*:\s*') {
|
||
$inPresets = $line -match '^agent-presets:\s*(?:#.*)?$'
|
||
}
|
||
if ($inPresets -and $line -match '^\s+default:\s*["'']?agentkey["'']?\s*(?:#.*)?$') {
|
||
$removedDefault = $true
|
||
continue
|
||
}
|
||
[void]$out.Add($line)
|
||
}
|
||
if ($removedDefault) {
|
||
[System.IO.File]::WriteAllLines($dshSettings, $out, $utf8NoBom)
|
||
Write-Ok "Removed legacy agent-presets default from $dshSettings"
|
||
}
|
||
}
|
||
# ── 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
|
||
if ($dshCleaned) { Write-Host ' Running DSH profiles watch removal; close legacy sessions or restart once if needed.' -ForegroundColor White }
|
||
else { Write-Host ' Restart your agent to apply changes.' -ForegroundColor White }
|
||
Write-Host ''
|