From 0510c74684f10ccadd059d1e0c27077f0f807891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?= <31078449+Nowhitestar@users.noreply.github.com> Date: Wed, 13 May 2026 14:03:20 +0800 Subject: [PATCH 1/2] feat(installer): unify skill + MCP agent registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive both `npx skills add -a` and `@agentkey/mcp --auth-login --only` from a single detected-agent list so MCP registration follows the same per-host auto-detection we already do for skill install. Lifts MCP auto-registration from 3 clients (Claude Code, Claude Desktop on mac/win, Cursor) to 16 — see `MCP_AUTO_AGENTS` in install.sh / `$McpAutoAgents` in install.ps1, mirroring `AGENT_REGISTRY` in AgentKey-Server/mcp-server/src/lib/mcp-clients.ts. Highlights: * Split `claude-desktop` out of the `claude-code` marker. The old marker treated `~/Library/Application Support/Claude` (Desktop's config dir) as a `claude-code` hit, which made `skills add -a claude-code` target a nonexistent Claude Code on Desktop-only machines. Now claude-desktop is its own id, listed in `MCP_ONLY_AGENTS` so it's passed to `--auth-login --only` but never to `skills add -a` (which would error — Claude Desktop has no skill install path). * Detect Claude Desktop two ways: app bundle (`/Applications/Claude.app` on mac, `%LOCALAPPDATA%\AnthropicClaude` on win) OR config dir. Covers "installed but never launched" where the dir doesn't exist yet. Linux still requires the config dir. * New edge-case branch in both scripts: `--only claude-desktop` (or any selection of only MCP-only ids) now SKIPS the skill step entirely instead of falling through to `skills add -a` with no filter — which was a silent target-list-defeat bug. * Pass `--only $MCP_TARGETS` to `--auth-login`. Older `@agentkey/mcp` versions silently ignore unknown flags, so this is forward-compatible. Uninstaller (the bigger gap): * Extend MCP config cleanup from 3 paths to 14 (codex TOML included). * JSON scrub is now schema-agnostic — walks the tree and drops any dict key whose name EXACTLY matches our server name (current + legacy), so the same scrubber handles `mcpServers.`, `mcp.`, `amp.mcpServers.`, and `projects.X.mcpServers.` in one pass. Exact-match (not substring) preserves user keys like `agentkey-helper`. * Codex TOML cleanup via awk / PowerShell line-splice — drops the `[mcp_servers.agentkey]` block (bare + legacy quoted name) without needing a TOML parser dep, preserving sibling sections. * `droid mcp remove` / `openclaw mcp unset` for the CLI-registered agents — they have no file-edit path so we have to unregister via CLI. Bug fixes spotted during review: * install.sh:487 referenced an unbound `$TARGETS` variable (renamed to `$ALL_TARGETS` during the refactor); `set -u` would have made this fatal. Fixed. * install.ps1 used `$SkillTargets.Count -gt 0` for the `-y` decision while install.sh used `$ALL_TARGETS`; the divergence meant the same `--only` invocation behaved differently on mac vs Windows. Aligned to AllTargets. * `_filter_csv` / `detect_agents` had leaked-scope loop vars; declared them `local` to prevent caller pollution. New: scripts/dev-smoke.sh — sandboxed regression suite, 42 assertions across 4 phases (unit tests / installer / writer schemas / uninstaller + false-positive guard). Runs in under 10s, never touches real $HOME. Use before opening PRs touching install* or uninstall* scripts. --- .claude/CLAUDE.md | 2 +- scripts/dev-smoke.sh | 478 ++++++++++++++++++++++++++++++++++++++++++ scripts/install.ps1 | 188 +++++++++++------ scripts/install.sh | 186 ++++++++++++---- scripts/uninstall.ps1 | 136 +++++++++--- scripts/uninstall.sh | 118 +++++++++-- 6 files changed, 953 insertions(+), 155 deletions(-) create mode 100755 scripts/dev-smoke.sh diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 30aba0b..fe3948e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 ` 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 ` 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 diff --git a/scripts/dev-smoke.sh b/scripts/dev-smoke.sh new file mode 100755 index 0000000..c2b79eb --- /dev/null +++ b/scripts/dev-smoke.sh @@ -0,0 +1,478 @@ +#!/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 + +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/) +# 4. Fall back to the env var as-is so the error message points at it +_find_cli_src() { + local cand + for cand in \ + "${AGENTKEY_CLI_SRC:-}" \ + "$SKILL_REPO/../AgentKey-Server/cli" \ + "$SKILL_REPO/../../../../AgentKey-Server/cli" \ + "$HOME/Documents/Codebase/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" < " + 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="$(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. 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..." + 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. 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. + 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 "$@" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b887cba..e913594 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -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.' } -# ── 2. Install the AgentKey skill ───────────────────────────────────────── -if (-not $SkipSkill) { - 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 - # 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)" +# 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 , or use -AllAgents)' } else { - $targets = Get-DetectedAgents - if ($targets.Count -gt 0) { - Write-Ok "Detected agents on this host: $($targets -join ', ')" - Write-Muted '(override with -Only , or use -AllAgents)' - } else { - Write-Info "No agents auto-detected — letting 'skills' CLI scan." - } + 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 ($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' $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%\., but amp / crush / goose / opencode live + # under %APPDATA%\. $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/) + (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' diff --git a/scripts/install.sh b/scripts/install.sh index c7c6d1b..4827743 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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: |[,...] # 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" - # ── 2. Install the AgentKey skill ───────────────────────────────────── - if ! $SKIP_SKILL; then - 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 - # 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)" + # ── 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 , or use --all-agents)" else - TARGETS="$(detect_agents)" - if [ -n "$TARGETS" ]; then - ui_ok "Detected agents on this host: $TARGETS" - ui_muted "(override with --only , or use --all-agents)" - else - ui_info "No agents auto-detected — letting 'skills' CLI scan." + 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 + 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" 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" diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 93330d3..041f12b 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -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.) + (Join-Path $env:APPDATA 'amp\settings.json'), # Amp (amp.mcpServers.) + (Join-Path $env:APPDATA 'crush\crush.json') # Crush (mcp.) +) + +# TOML config — handled separately (no built-in TOML parser pre-PS7.4). +$mcpTomlConfigs = @( + (Join-Path $home2 '.codex\config.toml') # Codex CLI ) -function Clean-McpConfig($path) { +$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)' diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 9110537..283ff12 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -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.) + "$HOME/.config/amp/settings.json" # Amp (amp.mcpServers.) + "$HOME/.config/crush/crush.json" # Crush (mcp.) ) 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)" From 4e0d8b90d142eaf7761507455236ab661b825bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?= <31078449+Nowhitestar@users.noreply.github.com> Date: Thu, 14 May 2026 16:18:08 +0800 Subject: [PATCH 2/2] fix(dev-smoke): harden runner path handling --- scripts/dev-smoke.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/dev-smoke.sh b/scripts/dev-smoke.sh index c2b79eb..b46df9a 100755 --- a/scripts/dev-smoke.sh +++ b/scripts/dev-smoke.sh @@ -22,6 +22,8 @@ # 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 ────────────────────────────────────────────────────────── @@ -33,14 +35,13 @@ SKILL_REPO="$(dirname "$SCRIPT_DIR")" # 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/) -# 4. Fall back to the env var as-is so the error message points at it +# 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" \ - "$HOME/Documents/Codebase/AgentKey-Server/cli" + "$SKILL_REPO/../../../../AgentKey-Server/cli" do [ -z "$cand" ] && continue if [ -d "$cand" ] && [ -f "$cand/package.json" ]; then @@ -226,8 +227,14 @@ phase_3() { # need real `droid` / `openclaw` binaries on PATH). info "running writers for every JSON / TOML agent..." local runner="$SANDBOX/runner.mjs" - cat > "$runner" < "$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" }; @@ -243,7 +250,7 @@ console.log("---"); console.log("written=" + written + " failed=" + failed); EOF local writer_out - writer_out="$(HOME="$SANDBOX" node "$runner" 2>&1)" + 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 ' @@ -293,7 +300,7 @@ EOF # Idempotency check — re-run all writers and make sure nothing duplicates. info "idempotency: re-running every writer..." - HOME="$SANDBOX" node "$runner" >/dev/null 2>&1 + 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