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
+1 -1
View File
@@ -75,6 +75,6 @@ Releases are driven by [release-please](https://github.com/googleapis/release-pl
## Architecture Constraints
- Setup mode in SKILL.md runs `! npx -y @agentkey/cli --auth-login` to authenticate via browser — same command as step 2 of the public install
- `@agentkey/cli --auth-login` auto-writes configs for Claude Code, Claude Desktop (mac/win), and Cursor only. Other agents need a manual JSON paste SKILL.md's "Fallback" section covers this; keep it up to date with any new auto-targets added server-side
- `@agentkey/cli --auth-login` auto-writes MCP configs for 16 agents (canonical list lives in `AGENT_REGISTRY` in `../AgentKey-Server/cli/src/lib/mcp-clients.ts`): Claude Code, Claude Desktop, Cursor, Codex, Gemini CLI, OpenCode, Qwen Code, iFlow CLI, Kimi CLI, Kiro CLI, Windsurf, Warp, Amp, Crush, droid, openclaw. The `--only <ids>` flag (used by install.sh's `MCP_TARGETS` and install.ps1's `$McpTargets`) filters this list — its id values MUST match `npx skills add -a` ids, with `claude-desktop` as the one documented MCP-only exception. Goose / kode / kilo still need a manual JSON paste (see SKILL.md's "Fallback" section); when adding more agents server-side, keep `MCP_AUTO_AGENTS` in both install scripts and the cleanup list in both uninstall scripts in sync.
- `.mcp.json` registers the remote-HTTP MCP endpoint (`https://api.agentkey.app/v1/mcp`) in Claude Code plugin mode; API key flows from plugin userConfig → `Authorization: Bearer <key>` header (no stdio binary is launched)
- `README.md` / `docs/README_zh.md` are the public-facing docs; keep them in sync with any structural changes
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env bash
#
# dev-smoke.sh — sandboxed regression suite for installer + uninstaller + MCP
# writer strategies. Nothing in this script touches your real $HOME — every
# phase creates its own mktemp -d sandbox and tears it down on exit.
#
# Run before opening a PR that changes:
# - scripts/install.sh / install.ps1
# - scripts/uninstall.sh / uninstall.ps1
# - AgentKey-Server/cli/src/lib/mcp-clients.ts
# - The AGENT_REGISTRY contract in general
#
# Usage:
# scripts/dev-smoke.sh # run all phases
# scripts/dev-smoke.sh 1 # run only phase 1
# scripts/dev-smoke.sh 2 4 # run phases 2 and 4
# AGENTKEY_CLI_SRC=/path/to/cli scripts/dev-smoke.sh # override
#
# Phases:
# 1. Unit tests — npm test in cli/ (registry + skill-meta)
# 2. Installer sandbox — --list-agents, --only edge cases (no real npx work)
# 3. Writers sandbox — call every AGENT_REGISTRY writer; assert schemas
# 4. Uninstaller sandbox — scrub fakes; assert decoys preserved
# Keep `-e` off so one failing assertion does not stop the smoke run before the
# remaining phases report their own failures.
set -uo pipefail
# ── Locate repos ──────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_REPO="$(dirname "$SCRIPT_DIR")"
# Find the sibling AgentKey-Server/cli repo (the npm-published @agentkey/cli
# source). Try in order:
# 1. $AGENTKEY_CLI_SRC env var (manual override)
# 2. Direct sibling of skill repo (main worktree case)
# 3. Climb up from a git worktree (3 levels up from .claude/worktrees/<name>)
# 4. Fall back to a generic sibling path so the error message is actionable
_find_cli_src() {
local cand
for cand in \
"${AGENTKEY_CLI_SRC:-}" \
"$SKILL_REPO/../AgentKey-Server/cli" \
"$SKILL_REPO/../../../../AgentKey-Server/cli"
do
[ -z "$cand" ] && continue
if [ -d "$cand" ] && [ -f "$cand/package.json" ]; then
(cd "$cand" && pwd); return
fi
done
# Nothing found — return the first candidate so the error message
# tells the user what we looked for.
printf '%s\n' "${AGENTKEY_CLI_SRC:-$SKILL_REPO/../AgentKey-Server/cli}"
}
CLI_SRC="$(_find_cli_src)"
# ── UI ────────────────────────────────────────────────────────────────────
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
GREEN=$'\033[38;2;0;220;150m'; RED=$'\033[38;2;230;57;70m'
YELLOW=$'\033[38;2;255;176;32m'; CYAN=$'\033[38;2;0;200;180m'
DIM=$'\033[38;2;110;118;132m'; BOLD=$'\033[1m'; NC=$'\033[0m'
else
GREEN=''; RED=''; YELLOW=''; CYAN=''; DIM=''; BOLD=''; NC=''
fi
PASS=0; FAIL=0; SKIP=0
FAIL_REASONS=()
ok() { PASS=$((PASS+1)); printf " ${GREEN}${NC} %s\n" "$*"; }
fail() { FAIL=$((FAIL+1)); FAIL_REASONS+=("$*"); printf " ${RED}${NC} %s\n" "$*"; }
skip() { SKIP=$((SKIP+1)); printf " ${DIM}-${NC} %s\n" "$*"; }
info() { printf " ${DIM}${NC} %s\n" "$*"; }
phase() { printf "\n${CYAN}${BOLD}── %s ──${NC}\n" "$*"; }
assert_contains() {
local name="$1" needle="$2" haystack="$3"
if printf '%s' "$haystack" | grep -qF -- "$needle"; then
ok "$name"
else
fail "$name (expected to contain '$needle')"
fi
}
assert_not_contains() {
local name="$1" needle="$2" haystack="$3"
if printf '%s' "$haystack" | grep -qF -- "$needle"; then
fail "$name (must NOT contain '$needle')"
else
ok "$name"
fi
}
assert_file_has() {
local name="$1" path="$2" needle="$3"
if [ -f "$path" ] && grep -qF -- "$needle" "$path"; then
ok "$name"
else
fail "$name (file $path missing or doesn't contain '$needle')"
fi
}
# Sandbox helper: each phase calls `sandbox_init` to get a fresh tmpdir and
# `sandbox_clean` to tear it down. Trapped so Ctrl-C still cleans up.
SANDBOX=""
sandbox_init() {
SANDBOX="$(mktemp -d -t agentkey-smoke.XXXXXX)"
info "sandbox: $SANDBOX"
}
sandbox_clean() {
[ -n "$SANDBOX" ] && [ -d "$SANDBOX" ] && rm -rf "$SANDBOX"
SANDBOX=""
}
trap 'sandbox_clean' EXIT INT TERM
# ──────────────────────────────────────────────────────────────────────────
# Phase 1: Unit tests
# ──────────────────────────────────────────────────────────────────────────
phase_1() {
phase "Phase 1: Unit tests (cli)"
if [ ! -d "$CLI_SRC" ]; then
skip "@agentkey/cli source not found at $CLI_SRC — set AGENTKEY_CLI_SRC to override"
return
fi
# Subshells under nvm often fall back to an older default Node, even when
# the parent shell has a current one. `node:test` (used by tsx --test)
# needs Node 18+. Detect explicitly and skip with a hint rather than
# surfacing a cryptic "bad option: --test" error.
local node_major
node_major="$(cd "$CLI_SRC" && node --version 2>/dev/null | sed -nE 's/^v([0-9]+).*/\1/p')"
if [ -z "$node_major" ]; then
skip "node not found in cli subshell"
return
fi
if [ "$node_major" -lt 18 ] 2>/dev/null; then
skip "subshell Node is v$node_major (need >=18). Try: \`nvm alias default 20\` or \`nvm use 20\` then re-run."
return
fi
info "running: cd $CLI_SRC && npm test (node v$node_major)"
local out
if out="$(cd "$CLI_SRC" && npm test 2>&1)"; then
local count
count="$(printf '%s' "$out" | sed -nE 's/.*tests ([0-9]+).*/\1/p' | head -1)"
if printf '%s' "$out" | grep -qE 'fail (0|0$)'; then
ok "all $count tests pass"
else
fail "npm test reported failures"
printf '%s\n' "$out" | tail -10
fi
else
fail "npm test exited non-zero"
if printf '%s' "$out" | grep -q 'bad option: --test'; then
info "hint: a child process picked up an older Node. Check \`which node\` inside $CLI_SRC."
fi
printf '%s\n' "$out" | tail -15
fi
}
# ──────────────────────────────────────────────────────────────────────────
# Phase 2: Installer sandbox — exercise install.sh logic without network
# ──────────────────────────────────────────────────────────────────────────
phase_2() {
phase "Phase 2: Installer sandbox"
sandbox_init
# Seed enough markers so detect_agents() finds a varied set.
mkdir -p "$SANDBOX/.cursor" \
"$SANDBOX/.codex" \
"$SANDBOX/.gemini" \
"$SANDBOX/.qwen" \
"$SANDBOX/Library/Application Support/Claude"
touch "$SANDBOX/.claude.json"
# Test 1: --list-agents output shape
info "test: --list-agents"
local listed
listed="$(HOME="$SANDBOX" bash "$SKILL_REPO/scripts/install.sh" --list-agents 2>&1)"
assert_contains "lists claude-code" "claude-code" "$listed"
assert_contains "lists claude-desktop" "claude-desktop" "$listed"
assert_contains "lists cursor" "cursor" "$listed"
assert_contains "lists codex" "codex" "$listed"
assert_contains "lists gemini-cli" "gemini-cli" "$listed"
assert_contains "lists qwen-code" "qwen-code" "$listed"
# Test 2: --only claude-desktop must skip the skill step (MCP-only edge case)
info "test: --only claude-desktop --skip-mcp --yes (edge case)"
local out
out="$(HOME="$SANDBOX" bash "$SKILL_REPO/scripts/install.sh" \
--only claude-desktop --skip-mcp --yes 2>&1)"
assert_contains "skill step skipped for MCP-only ids" \
"MCP-only (no skill install path)" "$out"
assert_contains "MCP step skipped via --skip-mcp" "Skipped (--skip-mcp)" "$out"
assert_not_contains "no skills add invocation" "Running: npx" "$out"
# Test 3: auto-detect path doesn't crash under set -u (the $TARGETS bug repro)
info "test: auto-detect with --skip-skill --skip-mcp --yes"
out="$(HOME="$SANDBOX" bash "$SKILL_REPO/scripts/install.sh" \
--skip-skill --skip-mcp --yes 2>&1)"
assert_contains "auto-detect runs" "Detected agents on this host" "$out"
assert_contains "claude-desktop detected" "claude-desktop" "$out"
assert_not_contains "no unbound var error" "unbound variable" "$out"
sandbox_clean
}
# ──────────────────────────────────────────────────────────────────────────
# Phase 3: Writers sandbox — exercise every AGENT_REGISTRY writer
# ──────────────────────────────────────────────────────────────────────────
phase_3() {
phase "Phase 3: MCP writer schemas"
if [ ! -d "$CLI_SRC" ]; then
skip "MCP server not at $CLI_SRC — skipping writer tests"
return
fi
# Ensure dist/ is fresh.
info "rebuilding cli dist/..."
if ! (cd "$CLI_SRC" && npm run build >/dev/null 2>&1); then
fail "npm run build failed"
return
fi
sandbox_init
# Run every file-write strategy (skip CLI-only ones since those would
# need real `droid` / `openclaw` binaries on PATH).
info "running writers for every JSON / TOML agent..."
local runner="$SANDBOX/runner.mjs"
cat > "$runner" <<'EOF'
import { pathToFileURL } from "node:url";
const cliModule = process.env.AGENTKEY_CLI_MODULE;
if (!cliModule) {
throw new Error("AGENTKEY_CLI_MODULE is required");
}
const { AGENT_REGISTRY, writeAgentConfig } = await import(pathToFileURL(cliModule).href);
const SKIP_IDS = new Set(["claude-code", "droid", "openclaw"]);
const ctx = { apiKey: "ak_test_smoke", baseUrl: "https://api.agentkey.app" };
let written = 0, failed = 0;
for (const spec of AGENT_REGISTRY) {
if (SKIP_IDS.has(spec.id)) { console.log("SKIP " + spec.id); continue; }
const r = await writeAgentConfig(spec.id, ctx);
if (r.ok) { written++; console.log("OK " + spec.id + " -> " + r.detail); }
else { failed++; console.log("ERR " + spec.id + " -> " + r.error); }
}
console.log("---");
console.log("written=" + written + " failed=" + failed);
EOF
local writer_out
writer_out="$(AGENTKEY_CLI_MODULE="$CLI_SRC/dist/lib/mcp-clients.js" HOME="$SANDBOX" node "$runner" 2>&1)"
if printf '%s' "$writer_out" | grep -q '^ERR '; then
fail "some writers reported errors:"
printf '%s\n' "$writer_out" | grep '^ERR '
else
ok "every writer reported success"
fi
# Schema assertions on the files that landed.
info "asserting per-agent schemas..."
# Claude Desktop / Cursor / Gemini / Windsurf / Warp / Qwen / iFlow /
# Kimi / Kiro all use the same {mcpServers: {agentkey: {command, args, env}}}
# shape — spot-check a couple.
assert_file_has "claude-desktop mcpServers.agentkey" \
"$SANDBOX/Library/Application Support/Claude/claude_desktop_config.json" \
'"agentkey"'
assert_file_has "cursor command=npx" \
"$SANDBOX/.cursor/mcp.json" '"command": "npx"'
assert_file_has "gemini-cli env API_KEY" \
"$SANDBOX/.gemini/settings.json" '"AGENTKEY_API_KEY": "ak_test_smoke"'
# OpenCode — the schema differs: array command + 'environment' (not 'env'),
# under top-level 'mcp' (not 'mcpServers').
local oc="$SANDBOX/.config/opencode/opencode.json"
assert_file_has "opencode uses 'mcp' key" "$oc" '"mcp":'
assert_file_has "opencode command is array" "$oc" '"command": ['
assert_file_has "opencode uses 'environment' key" "$oc" '"environment":'
if grep -q '"mcpServers"' "$oc" 2>/dev/null; then
fail "opencode must NOT use mcpServers key"
else
ok "opencode does not leak mcpServers"
fi
# Amp — flat dotted key, not nested.
local amp="$SANDBOX/.config/amp/settings.json"
assert_file_has "amp uses flat 'amp.mcpServers' key" "$amp" '"amp.mcpServers":'
# Crush — mcp.<name> with type:stdio.
local crush="$SANDBOX/.config/crush/crush.json"
assert_file_has "crush uses 'mcp' key" "$crush" '"mcp":'
assert_file_has "crush has type:stdio" "$crush" '"type": "stdio"'
# Codex — TOML.
local codex="$SANDBOX/.codex/config.toml"
assert_file_has "codex header [mcp_servers.agentkey]" "$codex" '[mcp_servers.agentkey]'
assert_file_has "codex env table" "$codex" 'AGENTKEY_API_KEY = "ak_test_smoke"'
# Idempotency check — re-run all writers and make sure nothing duplicates.
info "idempotency: re-running every writer..."
AGENTKEY_CLI_MODULE="$CLI_SRC/dist/lib/mcp-clients.js" HOME="$SANDBOX" node "$runner" >/dev/null 2>&1
local agentkey_count
agentkey_count="$(grep -c '\[mcp_servers\.agentkey\]' "$codex" 2>/dev/null)"
if [ "$agentkey_count" = "1" ]; then
ok "codex stanza appears exactly once after double-write"
else
fail "codex agentkey stanza count after double-write: $agentkey_count (expected 1)"
fi
sandbox_clean
}
# ──────────────────────────────────────────────────────────────────────────
# Phase 4: Uninstaller sandbox — assert decoys are preserved
# ──────────────────────────────────────────────────────────────────────────
phase_4() {
phase "Phase 4: Uninstaller sandbox"
sandbox_init
# ── Decoy fixtures ────────────────────────────────────────────────────
# Each fixture mixes a legit agentkey entry with one we MUST NOT touch.
mkdir -p "$SANDBOX/.cursor" \
"$SANDBOX/.config/opencode" \
"$SANDBOX/.config/amp" \
"$SANDBOX/.config/crush" \
"$SANDBOX/.codex" \
"$SANDBOX/.gemini"
# Cursor: standard mcpServers + a decoy user key containing "agentkey".
cat > "$SANDBOX/.cursor/mcp.json" <<'EOF'
{
"mcpServers": {
"agentkey": { "command": "npx", "args": ["-y", "@agentkey/cli"] },
"agentkey-helper": { "command": "x" },
"other-svr": { "command": "y" }
}
}
EOF
# OpenCode: mcp.<name> dialect.
cat > "$SANDBOX/.config/opencode/opencode.json" <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"agentkey": { "type": "local", "command": ["npx", "-y", "@agentkey/cli"] },
"user-svr": { "type": "local", "command": ["x"] }
}
}
EOF
# Amp: flat dotted key.
cat > "$SANDBOX/.config/amp/settings.json" <<'EOF'
{
"amp.mcpServers": {
"agentkey": { "command": "npx" },
"my-svr": { "command": "y" }
}
}
EOF
# Crush: mcp.<name> + type:stdio.
cat > "$SANDBOX/.config/crush/crush.json" <<'EOF'
{
"mcp": {
"agentkey": { "type": "stdio", "command": "npx" },
"keep": { "type": "stdio", "command": "z" }
}
}
EOF
# Codex TOML: agentkey block + legacy quoted block + unrelated sections.
cat > "$SANDBOX/.codex/config.toml" <<'EOF'
model = "gpt-5.1"
[mcp_servers.other]
command = "other"
args = []
[mcp_servers.agentkey]
command = "npx"
args = ["-y", "@agentkey/cli"]
env = { AGENTKEY_API_KEY = "ak_xxx" }
[mcp_servers."agentkey.app AgentKey"]
command = "legacy"
[unrelated_section]
key = "val"
EOF
# Gemini: claude-code per-project shape simulator (nested).
cat > "$SANDBOX/.claude.json" <<'EOF'
{
"mcpServers": { "agentkey": { "command": "npx" } },
"projects": {
"/path/a": {
"mcpServers": {
"agentkey": { "command": "npx" },
"user-server": { "command": "keep" }
}
}
}
}
EOF
# ── Run the uninstaller in the sandbox ────────────────────────────────
info "running uninstall.sh --skip-skill-remove --force-in-repo"
HOME="$SANDBOX" bash "$SKILL_REPO/scripts/uninstall.sh" \
--skip-skill-remove --force-in-repo >/dev/null 2>&1 || true
# ── Assertions: agentkey gone, decoys preserved ───────────────────────
info "checking: agentkey entries scrubbed"
assert_not_contains "cursor: agentkey removed" '"agentkey":' "$(cat "$SANDBOX/.cursor/mcp.json")"
assert_not_contains "opencode: agentkey removed" '"agentkey":' "$(cat "$SANDBOX/.config/opencode/opencode.json")"
assert_not_contains "amp: agentkey removed" '"agentkey":' "$(cat "$SANDBOX/.config/amp/settings.json")"
assert_not_contains "crush: agentkey removed" '"agentkey":' "$(cat "$SANDBOX/.config/crush/crush.json")"
assert_not_contains "codex: agentkey block removed" \
'[mcp_servers.agentkey]' "$(cat "$SANDBOX/.codex/config.toml")"
assert_not_contains "codex: legacy block removed" \
'agentkey.app AgentKey' "$(cat "$SANDBOX/.codex/config.toml")"
info "checking: decoy user entries preserved"
assert_contains "cursor: agentkey-helper kept (false-positive guard)" \
"agentkey-helper" "$(cat "$SANDBOX/.cursor/mcp.json")"
assert_contains "cursor: other-svr kept" \
"other-svr" "$(cat "$SANDBOX/.cursor/mcp.json")"
assert_contains "opencode: user-svr kept" \
"user-svr" "$(cat "$SANDBOX/.config/opencode/opencode.json")"
assert_contains "amp: my-svr kept" \
"my-svr" "$(cat "$SANDBOX/.config/amp/settings.json")"
assert_contains "crush: keep kept" \
'"keep"' "$(cat "$SANDBOX/.config/crush/crush.json")"
assert_contains "codex: [mcp_servers.other] kept" \
"[mcp_servers.other]" "$(cat "$SANDBOX/.codex/config.toml")"
assert_contains "codex: [unrelated_section] kept" \
"[unrelated_section]" "$(cat "$SANDBOX/.codex/config.toml")"
assert_contains "codex: top-level model= kept" \
'model = "gpt-5.1"' "$(cat "$SANDBOX/.codex/config.toml")"
assert_contains "claude.json: per-project user-server kept" \
"user-server" "$(cat "$SANDBOX/.claude.json")"
sandbox_clean
}
# ──────────────────────────────────────────────────────────────────────────
# Driver
# ──────────────────────────────────────────────────────────────────────────
main() {
local selected=("$@")
if [ ${#selected[@]} -eq 0 ]; then
selected=(1 2 3 4)
fi
printf "${BOLD}AgentKey dev-smoke${NC}\n"
printf " ${DIM}skill repo:${NC} %s\n" "$SKILL_REPO"
printf " ${DIM}cli:${NC} %s\n" "$CLI_SRC"
for n in "${selected[@]}"; do
case "$n" in
1) phase_1 ;;
2) phase_2 ;;
3) phase_3 ;;
4) phase_4 ;;
*) echo "Unknown phase: $n (valid: 1-4)" >&2 ;;
esac
done
# ── Summary ───────────────────────────────────────────────────────────
printf "\n${BOLD}Summary${NC}\n"
printf " ${GREEN}pass:${NC} %d ${RED}fail:${NC} %d ${DIM}skip:${NC} %d\n" \
"$PASS" "$FAIL" "$SKIP"
if [ "$FAIL" -gt 0 ]; then
printf "\n${RED}${BOLD}Failures:${NC}\n"
for r in "${FAIL_REASONS[@]}"; do
printf " ${RED}-${NC} %s\n" "$r"
done
exit 1
fi
printf "\n${GREEN}${BOLD}All green.${NC} Ready to PR.\n"
exit 0
}
main "$@"
+123 -65
View File
@@ -39,25 +39,44 @@ $NodeMinMajor = 18
# Subset of vercel-labs/skills' 45 supported agent IDs that have reliable
# Windows-side markers. 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`.
# `claude-desktop` is the documented exception (no skill install path, but
# MCP config is writable) — it's listed below and used only for MCP --only.
$AgentMarkers = @(
@{ Id = 'claude-code'; Markers = @("path:$env:USERPROFILE\.claude.json", 'cmd:claude', "path:$env:APPDATA\Claude") }
@{ Id = 'cursor'; Markers = @("path:$env:USERPROFILE\.cursor", 'cmd:cursor', "path:$env:LOCALAPPDATA\Programs\cursor") }
@{ Id = 'codex'; Markers = @("path:$env:USERPROFILE\.codex", 'cmd:codex') }
@{ Id = 'gemini-cli'; Markers = @("path:$env:USERPROFILE\.gemini", 'cmd:gemini') }
@{ Id = 'opencode'; Markers = @("path:$env:USERPROFILE\.opencode", 'cmd:opencode') }
@{ Id = 'openclaw'; Markers = @("path:$env:USERPROFILE\.openclaw") }
@{ Id = 'qwen-code'; Markers = @("path:$env:USERPROFILE\.qwen", 'cmd:qwen') }
@{ Id = 'iflow-cli'; Markers = @("path:$env:USERPROFILE\.iflow", 'cmd:iflow') }
@{ Id = 'windsurf'; Markers = @("path:$env:USERPROFILE\.windsurf", 'cmd:windsurf') }
@{ Id = 'warp'; Markers = @("path:$env:USERPROFILE\.warp") }
@{ Id = 'amp'; Markers = @('cmd:amp') }
@{ Id = 'crush'; Markers = @('cmd:crush') }
@{ Id = 'goose'; Markers = @('cmd:goose') }
@{ Id = 'droid'; Markers = @('cmd:droid') }
@{ Id = 'kode'; Markers = @('cmd:kode') }
@{ Id = 'kilo'; Markers = @('cmd:kilo') }
@{ Id = 'kimi-cli'; Markers = @("path:$env:USERPROFILE\.kimi", 'cmd:kimi') }
@{ Id = 'kiro-cli'; Markers = @("path:$env:USERPROFILE\.kiro", 'cmd:kiro') }
@{ Id = 'claude-code'; Markers = @("path:$env:USERPROFILE\.claude.json", 'cmd:claude') }
@{ Id = 'claude-desktop'; Markers = @("path:$env:LOCALAPPDATA\AnthropicClaude", "path:$env:APPDATA\Claude\claude_desktop_config.json", "path:$env:APPDATA\Claude") }
@{ Id = 'cursor'; Markers = @("path:$env:USERPROFILE\.cursor", 'cmd:cursor', "path:$env:LOCALAPPDATA\Programs\cursor") }
@{ Id = 'codex'; Markers = @("path:$env:USERPROFILE\.codex", 'cmd:codex') }
@{ Id = 'gemini-cli'; Markers = @("path:$env:USERPROFILE\.gemini", 'cmd:gemini') }
@{ Id = 'opencode'; Markers = @("path:$env:APPDATA\opencode", "path:$env:USERPROFILE\.opencode", 'cmd:opencode') }
@{ Id = 'openclaw'; Markers = @("path:$env:USERPROFILE\.openclaw", 'cmd:openclaw') }
@{ Id = 'qwen-code'; Markers = @("path:$env:USERPROFILE\.qwen", 'cmd:qwen') }
@{ Id = 'iflow-cli'; Markers = @("path:$env:USERPROFILE\.iflow", 'cmd:iflow') }
@{ Id = 'windsurf'; Markers = @("path:$env:USERPROFILE\.codeium\windsurf", "path:$env:USERPROFILE\.windsurf", 'cmd:windsurf') }
@{ Id = 'warp'; Markers = @("path:$env:USERPROFILE\.warp") }
@{ Id = 'amp'; Markers = @("path:$env:APPDATA\amp", 'cmd:amp') }
@{ Id = 'crush'; Markers = @("path:$env:APPDATA\crush", 'cmd:crush') }
@{ Id = 'goose'; Markers = @("path:$env:APPDATA\goose", 'cmd:goose') }
@{ Id = 'droid'; Markers = @('cmd:droid') }
@{ Id = 'kode'; Markers = @('cmd:kode') }
@{ Id = 'kilo'; Markers = @('cmd:kilo') }
@{ Id = 'kimi-cli'; Markers = @("path:$env:USERPROFILE\.kimi", 'cmd:kimi') }
@{ Id = 'kiro-cli'; Markers = @("path:$env:USERPROFILE\.kiro", 'cmd:kiro') }
)
# Agent ids that are MCP-only (no skill install path). Never passed to
# `npx skills add -a`, only to `--auth-login --only`.
$McpOnlyAgents = @('claude-desktop')
# Agent ids whose MCP registration the installer can drive automatically.
# Mirror of MCP_AUTO_AGENTS in install.sh and AGENT_REGISTRY in
# AgentKey-Server/cli/src/lib/mcp-clients.ts. Keep these three in sync.
$McpAutoAgents = @(
'claude-code', 'claude-desktop', 'cursor', 'codex', 'gemini-cli',
'opencode', 'qwen-code', 'iflow-cli', 'kimi-cli', 'kiro-cli',
'windsurf', 'warp', 'amp', 'crush', 'droid', 'openclaw'
)
# ── UI helpers ────────────────────────────────────────────────────────────
@@ -232,39 +251,50 @@ if (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
Die 'npx not found after Node install — please reopen your terminal or reinstall Node.js.'
}
# Resolve target agent list — shared between the skill step and the MCP step.
# $AllTargets — every detected agent, including MCP-only ones (claude-desktop)
# $SkillTargets — $AllTargets minus MCP-only ids (those would fail `skills add`)
# $McpTargets — $AllTargets filtered to ids the MCP CLI knows how to write
$AllTargets = @()
if ($Only) {
$AllTargets = @($Only -split ',' | Where-Object { $_ -ne '' })
Write-Info "Targeting agents from -Only: $($AllTargets -join ', ')"
} elseif ($AllAgents) {
Write-Info "Installing for every agent the 'skills' CLI detects (-AllAgents)"
} else {
$AllTargets = @(Get-DetectedAgents)
if ($AllTargets.Count -gt 0) {
Write-Ok "Detected agents on this host: $($AllTargets -join ', ')"
Write-Muted '(override with -Only <ids>, or use -AllAgents)'
} else {
Write-Info "No agents auto-detected — letting 'skills' CLI scan."
}
}
$SkillTargets = @($AllTargets | Where-Object { $_ -notin $McpOnlyAgents })
$McpTargets = @($AllTargets | Where-Object { $_ -in $McpAutoAgents })
# ── 2. Install the AgentKey skill ─────────────────────────────────────────
if (-not $SkipSkill) {
if ($SkipSkill) {
Write-Step '2. Install the AgentKey skill'
Write-Muted 'Skipped (-SkipSkill)'
} elseif ($AllTargets.Count -gt 0 -and $SkillTargets.Count -eq 0) {
# 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."
Write-Step '2. Install the AgentKey skill'
Write-Muted "Skipped — selected targets ($($AllTargets -join ',')) are MCP-only (no skill install path)."
} else {
Write-Step '2. Install the AgentKey skill'
# Resolve target agent list:
# 1. -Only wins (manual override)
# 2. else -AllAgents ⇒ 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)
$targets = @()
if ($Only) {
$targets = $Only -split ',' | Where-Object { $_ -ne '' }
Write-Info "Targeting agents from -Only: $($targets -join ', ')"
} elseif ($AllAgents) {
Write-Info "Installing for every agent the 'skills' CLI detects (-AllAgents)"
} else {
$targets = Get-DetectedAgents
if ($targets.Count -gt 0) {
Write-Ok "Detected agents on this host: $($targets -join ', ')"
Write-Muted '(override with -Only <ids>, or use -AllAgents)'
} else {
Write-Info "No agents auto-detected — letting 'skills' CLI scan."
}
}
$skillsArgs = @('-y', 'skills', 'add', $SkillRepo, '-g')
if ($targets.Count -gt 0) {
if ($SkillTargets.Count -gt 0) {
$skillsArgs += '-a'
$skillsArgs += $targets
$skillsArgs += $SkillTargets
}
# 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 -eq 'noninteractive' -or $targets.Count -gt 0) {
if ($Mode -eq 'noninteractive' -or $AllTargets.Count -gt 0) {
$skillsArgs += '-y'
}
@@ -272,24 +302,34 @@ if (-not $SkipSkill) {
if ($LASTEXITCODE -ne 0) { Die "Failed to install skill via 'skills' CLI" }
# The skills CLI sometimes prints "Installation failed" and still
# exits 0 (e.g. network error during git clone). Verify the skill
# actually landed on disk before declaring success.
# actually landed on disk before declaring success. Paths must mirror
# the `path:` markers in $AgentMarkers: most agents live under
# %USERPROFILE%\.<agent>, but amp / crush / goose / opencode live
# under %APPDATA%\<agent>.
$userHome = [Environment]::GetFolderPath('UserProfile')
$candidatePaths = @(
'.agents\skills\agentkey',
'.claude\skills\agentkey',
'.cursor\skills\agentkey',
'.codex\skills\agentkey',
'.gemini\skills\agentkey',
'.opencode\skills\agentkey',
'.openclaw\skills\agentkey',
'.qwen\skills\agentkey',
'.iflow\skills\agentkey',
'.windsurf\skills\agentkey',
'.warp\skills\agentkey'
(Join-Path $userHome '.agents\skills\agentkey'),
(Join-Path $userHome '.claude\skills\agentkey'),
(Join-Path $userHome '.cursor\skills\agentkey'),
(Join-Path $userHome '.codex\skills\agentkey'),
(Join-Path $userHome '.gemini\skills\agentkey'),
(Join-Path $userHome '.opencode\skills\agentkey'),
(Join-Path $userHome '.openclaw\skills\agentkey'),
(Join-Path $userHome '.qwen\skills\agentkey'),
(Join-Path $userHome '.iflow\skills\agentkey'),
(Join-Path $userHome '.windsurf\skills\agentkey'),
(Join-Path $userHome '.warp\skills\agentkey'),
(Join-Path $userHome '.kimi\skills\agentkey'),
(Join-Path $userHome '.kiro\skills\agentkey'),
# APPDATA-rooted agents (parity with install.sh's $HOME/.config/<agent>)
(Join-Path $env:APPDATA 'amp\skills\agentkey'),
(Join-Path $env:APPDATA 'crush\skills\agentkey'),
(Join-Path $env:APPDATA 'goose\skills\agentkey'),
(Join-Path $env:APPDATA 'opencode\skills\agentkey')
)
$agentkeyFound = $false
foreach ($rel in $candidatePaths) {
if (Test-Path (Join-Path $userHome (Join-Path $rel 'SKILL.md'))) {
foreach ($abs in $candidatePaths) {
if (Test-Path (Join-Path $abs 'SKILL.md')) {
$agentkeyFound = $true
break
}
@@ -298,9 +338,6 @@ if (-not $SkipSkill) {
Die "Skill install reported success but no agentkey SKILL.md was created — likely a network or git clone failure. Retry: npx -y skills add $SkillRepo -g -y"
}
Write-Ok 'Skill installed'
} else {
Write-Step '2. Install the AgentKey skill'
Write-Muted 'Skipped (-SkipSkill)'
}
# ── 3. MCP authentication ────────────────────────────────────────────────
@@ -311,10 +348,32 @@ if (-not $SkipSkill) {
if ($SkipMcp) {
Write-Step '3. Register the MCP server'
Write-Muted 'Skipped (-SkipMcp)'
} elseif ($AllTargets.Count -gt 0 -and $McpTargets.Count -eq 0) {
# 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.
Write-Step '3. Register the MCP server'
Write-Muted "Skipped — selected agents ($($AllTargets -join ',')) need manual MCP setup (see SKILL.md Fallback section)."
} else {
# Pin MCP registration to the same agent list the skill step targeted.
# When McpTargets 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.
$authArgs = @('--auth-login')
if ($McpTargets.Count -gt 0) {
$authArgs += '--only'
$authArgs += ($McpTargets -join ',')
}
Write-Step '3. Register the MCP server'
Write-Info 'Opening your browser for AgentKey device authentication ...'
Write-Muted "If a browser doesn't open (SSH / WinRM / Docker / headless), the auth URL is also printed below — open it on any device to finish."
if ($McpTargets.Count -gt 0) {
Write-Muted "Will register MCP in: $($McpTargets -join ', ')"
} else {
Write-Muted "If a browser doesn't open (SSH / WinRM / Docker / headless), the auth URL is also printed below — open it on any device to finish."
}
Write-Host ''
# Telemetry context for install_completed. Opt-out is honored at the
@@ -336,19 +395,18 @@ if ($SkipMcp) {
$DeviceFingerprint = $_hash.Substring(0, 16)
$DetectedAgents = Get-DetectedAgents
if (-not (Get-Variable -Name targets -Scope Script -ErrorAction SilentlyContinue)) { $targets = @() }
$env:AGENTKEY_INSTALL_SOURCE = 'one_liner'
$env:AGENTKEY_DETECTED_AGENTS = ($DetectedAgents -join ',')
$env:AGENTKEY_SELECTED_AGENTS = ($targets -join ',')
$env:AGENTKEY_SELECTED_AGENTS = ($AllTargets -join ',')
$env:AGENTKEY_INSTALLER_FLAGS = ($PSBoundParameters.Keys | ForEach-Object { "-$_" }) -join ','
$env:AGENTKEY_DEVICE_FINGERPRINT = $DeviceFingerprint
}
& npx -y $CliPackage --auth-login
& npx -y $CliPackage @authArgs
if ($LASTEXITCODE -ne 0) {
Write-Err 'MCP auth failed.'
Write-Muted "Retry manually: npx -y $CliPackage --auth-login"
Write-Muted "Retry manually: npx -y $CliPackage $($authArgs -join ' ')"
exit 1
}
Write-Ok 'MCP server registered'
+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"
+103 -33
View File
@@ -95,13 +95,61 @@ if ($SkipSkillRemove) {
Write-Step '2. MCP server entries'
$home2 = [Environment]::GetFolderPath('UserProfile')
$mcpConfigs = @(
# 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 $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>)
)
function Clean-McpConfig($path) {
# 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
@@ -113,35 +161,7 @@ function Clean-McpConfig($path) {
Write-Warn2 "Could not parse $path — skipping"
return
}
$removed = 0
if ($obj.mcpServers) {
$keys = @($obj.mcpServers.PSObject.Properties.Name)
foreach ($k in $keys) {
if ($k -match 'agentkey') {
$obj.mcpServers.PSObject.Properties.Remove($k)
$removed++
}
}
}
# Per-project mcpServers (Claude Code ~/.claude.json shape)
if ($obj.projects) {
$projNames = @($obj.projects.PSObject.Properties.Name)
foreach ($pn in $projNames) {
$proj = $obj.projects.$pn
if ($proj.mcpServers) {
$pkeys = @($proj.mcpServers.PSObject.Properties.Name)
foreach ($k in $pkeys) {
if ($k -match 'agentkey') {
$proj.mcpServers.PSObject.Properties.Remove($k)
$removed++
}
}
}
}
}
$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"
@@ -150,7 +170,57 @@ function Clean-McpConfig($path) {
}
}
foreach ($cfg in $mcpConfigs) { Clean-McpConfig $cfg }
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)'
+101 -17
View File
@@ -96,24 +96,51 @@ fi
step "2. MCP server entries"
OS="$(uname -s)"
MCP_CONFIGS=(
# 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_CONFIGS+=("$HOME/Library/Application Support/Claude/claude_desktop_config.json")
MCP_JSON_CONFIGS+=("$HOME/Library/Application Support/Claude/claude_desktop_config.json")
else
MCP_CONFIGS+=("$HOME/.config/Claude/claude_desktop_config.json")
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_CONFIGS[@]}"; do [ -f "$f" ] && echo " $f"; done
for f in "${MCP_JSON_CONFIGS[@]}"; do [ -f "$f" ] && echo " $f"; done
else
for cfg in "${MCP_CONFIGS[@]}"; do
for cfg in "${MCP_JSON_CONFIGS[@]}"; do
if [ ! -f "$cfg" ]; then
skipped "$(basename "$cfg") not found"
continue
@@ -126,18 +153,27 @@ try:
except Exception as e:
print(f"ERROR: {e}"); sys.exit(0)
removed = 0
# Top-level mcpServers.agentkey*
if isinstance(d, dict):
for k in list(d.get('mcpServers', {}).keys()):
if 'agentkey' in k.lower():
del d['mcpServers'][k]; removed += 1
# Per-project entries (Claude Code ~/.claude.json shape)
for proj in d.get('projects', {}).values():
if not isinstance(proj, dict): continue
for k in list(proj.get('mcpServers', {}).keys()):
if 'agentkey' in k.lower():
del proj['mcpServers'][k]; removed += 1
# 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)
@@ -154,6 +190,54 @@ EOF
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)"