mirror of
https://github.com/chainbase-labs/Agentkey.git
synced 2026-09-20 14:20:23 +08:00
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:
+1
-1
@@ -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
|
||||
|
||||
Executable
+485
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user