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>
This commit is contained in:
不白
2026-05-20 10:07:11 +08:00
committed by GitHub
parent 4ff03c11f8
commit 83363014c4
6 changed files with 961 additions and 156 deletions
+148 -40
View File
@@ -27,23 +27,33 @@ NODE_MIN_MAJOR=18
# pre-detected — the user can pass --all-agents or --only to include them.
# Sync source: https://github.com/vercel-labs/skills (Supported Agents table).
#
# IMPORTANT: ids here MUST match the `--only` ids accepted by both
# `npx skills add -a` and `npx -y @agentkey/cli --auth-login --only`.
# That alignment is what lets the installer drive both halves with one list.
#
# `claude-desktop` is the documented exception — it isn't in the skills CLI
# (Desktop installs skills into a sandbox path the CLI can't write), but
# Desktop's MCP config IS auto-writable, so we list it separately and pass
# it ONLY to the MCP --only filter (see SKILL_TARGETS / MCP_TARGETS below).
#
# 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"
"claude-code|path:~/.claude.json,cmd:claude"
"claude-desktop|path:/Applications/Claude.app,path:~/Applications/Claude.app,path:~/Library/Application Support/Claude/claude_desktop_config.json,path:~/Library/Application Support/Claude,path:~/.config/Claude/claude_desktop_config.json,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"
"opencode|path:~/.config/opencode,path:~/.opencode,cmd:opencode"
"openclaw|path:~/.openclaw,cmd:openclaw"
"qwen-code|path:~/.qwen,cmd:qwen"
"iflow-cli|path:~/.iflow,cmd:iflow"
"windsurf|path:~/.windsurf,cmd:windsurf"
"windsurf|path:~/.codeium/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"
"amp|path:~/.config/amp,cmd:amp"
"crush|path:~/.config/crush,cmd:crush"
"goose|path:~/.config/goose,cmd:goose"
"droid|cmd:droid"
"kode|cmd:kode"
"kilo|cmd:kilo"
@@ -51,6 +61,20 @@ AGENT_MARKERS=(
"kiro-cli|path:~/.kiro,cmd:kiro"
)
# Agent ids that are MCP-only (no skill install path). These get passed to
# `--auth-login --only` but NEVER to `npx skills add -a`.
MCP_ONLY_AGENTS=(claude-desktop)
# Agent ids whose MCP registration the installer can drive automatically.
# Skipped agents (goose / kode / kilo) still get the skill, but the user
# must register MCP manually for them. Keep this in sync with
# AGENT_REGISTRY in AgentKey-Server/cli/src/lib/mcp-clients.ts.
MCP_AUTO_AGENTS=(
claude-code claude-desktop cursor codex gemini-cli opencode
qwen-code iflow-cli kimi-cli kiro-cli windsurf warp
amp crush droid openclaw
)
# ── 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.
@@ -160,6 +184,35 @@ detect_agents() {
fi
}
# Membership helper: is "$1" in the rest of the argument list?
_in_list() {
local needle="$1"; shift
local item
for item in "$@"; do
[ "$item" = "$needle" ] && return 0
done
return 1
}
# Filter a comma-separated id list, keeping only ids that are passed in the
# remaining arguments. Output is comma-separated. Short-circuits on empty
# input so callers don't have to guard.
_filter_csv() {
local csv="$1"; shift
[ -z "$csv" ] && return 0
local id
local -a ids=() out=()
IFS=',' read -ra ids <<<"$csv"
for id in "${ids[@]}"; do
if _in_list "$id" "$@"; then
out+=("$id")
fi
done
if [ ${#out[@]} -gt 0 ]; then
printf '%s\n' "${out[@]}" | paste -sd, -
fi
}
install_node() {
local platform="$1"
ui_info "Installing Node.js v$NODE_MIN_MAJOR+ ..."
@@ -339,42 +392,73 @@ main() {
command -v npx >/dev/null 2>&1 || die "npx not found after Node install — please reinstall Node.js"
# ── Resolve target agent list ─────────────────────────────────────────
# Used by step 2 (skill) and step 3 (MCP). Computed once here so both
# halves see the same source of truth — that's the invariant the unified
# install+register design depends on. Two derived lists:
#
# ALL_TARGETS — every detected agent, including MCP-only ones (claude-desktop)
# SKILL_TARGETS — ALL_TARGETS minus MCP-only ids (those would error in `skills add`)
# MCP_TARGETS — ALL_TARGETS filtered to ids the MCP CLI knows how to write
local ALL_TARGETS=""
if [ -n "$ONLY_AGENTS" ]; then
ALL_TARGETS="$ONLY_AGENTS"
ui_info "Targeting agents from --only: $ALL_TARGETS"
elif $ALL_AGENTS; then
ui_info "Installing for every agent the 'skills' CLI detects (--all-agents)"
else
ALL_TARGETS="$(detect_agents)"
if [ -n "$ALL_TARGETS" ]; then
ui_ok "Detected agents on this host: $ALL_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 SKILL_TARGETS=""
local MCP_TARGETS=""
if [ -n "$ALL_TARGETS" ]; then
# SKILL_TARGETS: drop MCP-only ids (would fail in `skills add -a`).
local _id
local -a _id_list=() _kept=()
IFS=',' read -ra _id_list <<<"$ALL_TARGETS"
for _id in "${_id_list[@]}"; do
if ! _in_list "$_id" "${MCP_ONLY_AGENTS[@]}"; then
_kept+=("$_id")
fi
done
if [ ${#_kept[@]} -gt 0 ]; then
SKILL_TARGETS="$(printf '%s\n' "${_kept[@]}" | paste -sd, -)"
fi
# MCP_TARGETS: keep only ids the MCP CLI knows how to register.
MCP_TARGETS="$(_filter_csv "$ALL_TARGETS" "${MCP_AUTO_AGENTS[@]}")"
fi
# ── 2. Install the AgentKey skill ─────────────────────────────────────
if ! $SKIP_SKILL; then
if $SKIP_SKILL; then
ui_step "2. Install the AgentKey skill"
ui_muted "Skipped (--skip-skill)"
elif [ -n "$ALL_TARGETS" ] && [ -z "$SKILL_TARGETS" ]; then
# User explicitly selected only MCP-only ids (e.g. `--only claude-desktop`).
# There's nothing for `skills add` to do — skip the step entirely
# rather than fall through to "install for every detected agent."
ui_step "2. Install the AgentKey skill"
ui_muted "Skipped — selected targets ($ALL_TARGETS) are MCP-only (no skill install path)."
else
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
if [ -n "$SKILL_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"
IFS=',' read -ra AGENT_LIST <<<"$SKILL_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
if [ "$MODE" = noninteractive ] || [ -n "$ALL_TARGETS" ]; then
SKILLS_ARGS+=(-y)
fi
@@ -404,16 +488,19 @@ main() {
"$HOME/.qwen/skills/agentkey" \
"$HOME/.iflow/skills/agentkey" \
"$HOME/.windsurf/skills/agentkey" \
"$HOME/.warp/skills/agentkey"; do
"$HOME/.warp/skills/agentkey" \
"$HOME/.config/amp/skills/agentkey" \
"$HOME/.config/crush/skills/agentkey" \
"$HOME/.config/goose/skills/agentkey" \
"$HOME/.config/opencode/skills/agentkey" \
"$HOME/.kimi/skills/agentkey" \
"$HOME/.kiro/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 ────────────────────────────────────────────
@@ -424,10 +511,31 @@ main() {
if $SKIP_MCP; then
ui_step "3. Register the MCP server"
ui_muted "Skipped (--skip-mcp)"
elif [ -n "$ALL_TARGETS" ] && [ -z "$MCP_TARGETS" ]; then
# User selected ONLY MCP-incompatible agents (goose / kode / kilo
# via --only). Running auth-login without --only would silently
# register MCP in every detected agent — overriding the user's
# explicit scope. Skip rather than over-register. See PR #41 B1.
ui_step "3. Register the MCP server"
ui_muted "Skipped — selected agents ($ALL_TARGETS) need manual MCP setup (see SKILL.md Fallback section)."
else
# Pin MCP registration to the same agent list the skill step
# targeted. When MCP_TARGETS is empty (auto-detect found nothing),
# let `@agentkey/cli` do its own detection — same fallback we use
# for skill install. Older CLI versions silently ignore --only,
# so this is forward-compatible.
local AUTH_ARGS=(--auth-login)
if [ -n "$MCP_TARGETS" ]; then
AUTH_ARGS+=(--only "$MCP_TARGETS")
fi
ui_step "3. Register the MCP server"
ui_info "Opening your browser for AgentKey device authentication ..."
ui_muted "If a browser doesn't open (SSH / Docker / headless), the auth URL is also printed below — open it on any device to finish."
if [ -n "$MCP_TARGETS" ]; then
ui_muted "Will register MCP in: $MCP_TARGETS"
else
ui_muted "If a browser doesn't open (SSH / Docker / headless), the auth URL is also printed below — open it on any device to finish."
fi
echo
# Telemetry context for `install_completed`. Opt-out is honored at
@@ -446,14 +554,14 @@ main() {
done
export AGENTKEY_INSTALL_SOURCE="one_liner"
export AGENTKEY_DETECTED_AGENTS="$(detect_agents)"
export AGENTKEY_SELECTED_AGENTS="${TARGETS:-}"
export AGENTKEY_SELECTED_AGENTS="${ALL_TARGETS:-}"
export AGENTKEY_INSTALLER_FLAGS="$_flags"
export AGENTKEY_DEVICE_FINGERPRINT="$(compute_device_fingerprint "$PLATFORM")"
fi
if ! npx -y "$CLI_PACKAGE" --auth-login; then
if ! npx -y "$CLI_PACKAGE" "${AUTH_ARGS[@]}"; then
ui_error "MCP auth failed."
ui_muted "Retry manually: npx -y $CLI_PACKAGE --auth-login"
ui_muted "Retry manually: npx -y $CLI_PACKAGE ${AUTH_ARGS[*]}"
exit 1
fi
ui_ok "MCP server registered"