From 1de5045eab2cccd3d2df5a35ca725c0e7d5004ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?=
<31078449+Nowhitestar@users.noreply.github.com>
Date: Tue, 12 May 2026 14:11:38 +0800
Subject: [PATCH 1/3] fix(scripts): correct skill-name arg + detect silent
skills-CLI failures
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two related bugs in install.sh / uninstall.sh and their PowerShell twins,
both rooted in how the `npx skills` CLI handles bad inputs:
1. `skills remove` takes the **skill name** (`agentkey`), not the repo
path. uninstall.sh / uninstall.ps1 were passing `chainbase-labs/agentkey`,
so the CLI printed "No matching skills found" — but exited 0. The
uninstaller's `if exit-code == 0` branch then printed "✓ Skill removed
from detected agents" while the files stayed untouched. Switch the
argument to `agentkey` and gate the success message on the stdout
string instead of the exit code.
2. `skills add` exits 0 even when it prints "Installation failed" (e.g.
a network / git-clone error mid-run). install.sh / install.ps1 trusted
the exit code, so a failed install was reported as success. Add a
post-install check that scans the well-known per-agent skill paths
for `agentkey/SKILL.md` and aborts with a clear retry hint when none
exist.
Verified locally: passing the correct skill name now both reports success
and actually deletes ~/.agents/skills/agentkey and ~/.claude/skills/agentkey.
---
scripts/install.ps1 | 27 +++++++++++++++++++++++++++
scripts/install.sh | 21 +++++++++++++++++++++
scripts/uninstall.ps1 | 15 ++++++++++-----
scripts/uninstall.sh | 15 ++++++++++-----
4 files changed, 68 insertions(+), 10 deletions(-)
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index 6aed603..3469a70 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -295,6 +295,33 @@ if (-not $SkipSkill) {
& npx @skillsArgs
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.
+ $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'
+ )
+ $agentkeyFound = $false
+ foreach ($rel in $candidatePaths) {
+ if (Test-Path (Join-Path $userHome (Join-Path $rel 'SKILL.md'))) {
+ $agentkeyFound = $true
+ break
+ }
+ }
+ if (-not $agentkeyFound) {
+ 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'
diff --git a/scripts/install.sh b/scripts/install.sh
index 649ec7a..30009dc 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -411,6 +411,27 @@ main() {
if ! npx "${SKILLS_ARGS[@]}" < "$npx_stdin"; then
die "Failed to install skill via 'skills' CLI"
fi
+ # 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.
+ local _agentkey_found=false _dir
+ for _dir in \
+ "$HOME/.agents/skills/agentkey" \
+ "$HOME/.claude/skills/agentkey" \
+ "$HOME/.cursor/skills/agentkey" \
+ "$HOME/.codex/skills/agentkey" \
+ "$HOME/.gemini/skills/agentkey" \
+ "$HOME/.opencode/skills/agentkey" \
+ "$HOME/.openclaw/skills/agentkey" \
+ "$HOME/.qwen/skills/agentkey" \
+ "$HOME/.iflow/skills/agentkey" \
+ "$HOME/.windsurf/skills/agentkey" \
+ "$HOME/.warp/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"
diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1
index 7656a4b..46879ef 100644
--- a/scripts/uninstall.ps1
+++ b/scripts/uninstall.ps1
@@ -75,14 +75,19 @@ if ($SkipSkillRemove) {
Write-Skip 'Skipped (-SkipSkillRemove)'
} elseif (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
Write-Warn2 "npx not found — skipping 'skills remove'"
- Write-Host ' Manual: npx skills remove chainbase-labs/agentkey -g' -ForegroundColor DarkGray
+ Write-Host ' Manual: npx skills remove agentkey -g' -ForegroundColor DarkGray
} else {
- Write-Info 'Running: npx -y skills remove chainbase-labs/agentkey -g -y'
- & npx -y skills remove chainbase-labs/agentkey -g -y 2>$null
- if ($LASTEXITCODE -eq 0) {
+ # `skills remove` takes the **skill name** (`agentkey`), not the repo path.
+ # The CLI also exits 0 when nothing matches, so we inspect stdout instead.
+ Write-Info 'Running: npx -y skills remove agentkey -g -y'
+ $removeOutput = (& npx -y skills remove agentkey -g -y 2>&1) -join "`n"
+ if ($removeOutput -match 'Successfully removed') {
Write-Ok 'Skill removed from detected agents'
+ } elseif ($removeOutput -match 'No matching skills found') {
+ Write-Skip "Not registered with 'skills' CLI (already removed or installed via plugin marketplace)"
} else {
- Write-Warn2 "'skills remove' exited non-zero — some agents may still have skill files"
+ Write-Warn2 "'skills remove' produced unexpected output — some agents may still have skill files"
+ Write-Host ' Check manually: npx skills list -g' -ForegroundColor DarkGray
}
}
diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh
index 606f36c..7c24fcc 100755
--- a/scripts/uninstall.sh
+++ b/scripts/uninstall.sh
@@ -76,14 +76,19 @@ step "1. Skill files"
if $SKIP_SKILL_REMOVE; then
skipped "Skipped (--skip-skill-remove)"
elif ! command -v npx >/dev/null 2>&1; then
- warn "npx not found — skipping 'skills remove' (manual: npx skills remove chainbase-labs/agentkey -g)"
+ warn "npx not found — skipping 'skills remove' (manual: npx skills remove agentkey -g)"
else
- info "Running: npx -y skills remove chainbase-labs/agentkey -g -y"
- if npx -y skills remove chainbase-labs/agentkey -g -y 2>/dev/null; then
+ # `skills remove` takes the **skill name** (`agentkey`), not the repo path.
+ # The CLI also exits 0 when nothing matches, so we inspect stdout instead.
+ info "Running: npx -y skills remove agentkey -g -y"
+ REMOVE_OUTPUT="$(npx -y skills remove agentkey -g -y 2>&1 || true)"
+ if printf '%s\n' "$REMOVE_OUTPUT" | grep -q "Successfully removed"; then
ok "Skill removed from detected agents"
+ elif printf '%s\n' "$REMOVE_OUTPUT" | grep -q "No matching skills found"; then
+ skipped "Not registered with 'skills' CLI (already removed or installed via plugin marketplace)"
else
- warn "'skills remove' exited non-zero — some agents may still have skill files"
- warn "Check manually: npx skills list"
+ warn "'skills remove' produced unexpected output — some agents may still have skill files"
+ warn "Check manually: npx skills list -g"
fi
fi
From c56d0a43934e5c1d93defae44f3a888fd3476a14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?=
<31078449+Nowhitestar@users.noreply.github.com>
Date: Tue, 12 May 2026 15:59:58 +0800
Subject: [PATCH 2/3] feat(skill): server-beacon skill-update path for non-Bash
clients
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds the cross-client skill-update mechanism needed for Claude Desktop and
any other MCP client that can't execute the inline `bash` block in SKILL.md
Step 0. Without this, Desktop users get stuck on whatever skill version
shipped at first install (this machine had a sandboxed 0.1.2 from April).
How it works:
- A new MCP tool `agentkey_skill_meta` (implemented in the server repo,
spec'd here) returns structured JSON: latest skill version, detected
client, and a client-specific upgrade recipe.
- The skill rule (SKILL.md Step 0.A) calls it once per session and prompts
the user to upgrade when the response's version differs from the
frontmatter's. The legacy inline-bash check is retained as Step 0.B for
Bash-capable clients.
- Protocol is versioned (`protocol_version: 1`) with five immortal
fields, so v1 skills are never broken by future v2/v3 servers; they
fall back to `update_doc_url`.
Repo contents:
- protocol/skill-meta-v1.md — normative spec
- protocol/skill-meta-v1.schema.json — JSON Schema, source of truth
- protocol/example-response-*.json — fixtures covering all failure modes
- docs/SERVER-IMPLEMENTATION.md — implementation handoff for @agentkey/mcp
- .github/workflows/protocol-validate.yml — schema/fixture/cross-ref CI
- skills/agentkey/SKILL.md — rule for parsing the beacon (Step 0.A)
- README.md / docs/README_zh.md — Desktop upgrade section incl. one-time
bootstrap command for users currently stuck on a pre-1.4.0 skill
No behavior change for Claude Code users; the inline bash path is
unchanged. Desktop and similar clients get a working upgrade prompt
once @agentkey/mcp ships the corresponding tool.
---
.github/workflows/protocol-validate.yml | 84 +++++
README.md | 35 ++-
docs/README_zh.md | 35 ++-
docs/SERVER-IMPLEMENTATION.md | 290 ++++++++++++++++++
protocol/example-response-claude-code.json | 9 +
protocol/example-response-claude-desktop.json | 9 +
protocol/example-response-offline.json | 6 +
protocol/example-response-unknown-client.json | 6 +
protocol/skill-meta-v1.md | 174 +++++++++++
protocol/skill-meta-v1.schema.json | 55 ++++
skills/agentkey/SKILL.md | 52 +++-
11 files changed, 734 insertions(+), 21 deletions(-)
create mode 100644 .github/workflows/protocol-validate.yml
create mode 100644 docs/SERVER-IMPLEMENTATION.md
create mode 100644 protocol/example-response-claude-code.json
create mode 100644 protocol/example-response-claude-desktop.json
create mode 100644 protocol/example-response-offline.json
create mode 100644 protocol/example-response-unknown-client.json
create mode 100644 protocol/skill-meta-v1.md
create mode 100644 protocol/skill-meta-v1.schema.json
diff --git a/.github/workflows/protocol-validate.yml b/.github/workflows/protocol-validate.yml
new file mode 100644
index 0000000..af329b8
--- /dev/null
+++ b/.github/workflows/protocol-validate.yml
@@ -0,0 +1,84 @@
+name: protocol-validate
+
+# Validates that every example fixture under protocol/ conforms to
+# skill-meta-v1.schema.json. Catches the case where someone updates the schema
+# but forgets to update the fixtures (or vice versa). When @agentkey/mcp
+# eventually ships its vendored copy of the same schema, a second job here will
+# diff against it to detect drift in the other direction.
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'protocol/**'
+ - '.github/workflows/protocol-validate.yml'
+ pull_request:
+ paths:
+ - 'protocol/**'
+ - '.github/workflows/protocol-validate.yml'
+
+jobs:
+ validate-fixtures:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate every example fixture against the schema
+ run: |
+ set -euo pipefail
+ shopt -s nullglob
+ fixtures=(protocol/example-*.json)
+ if [ ${#fixtures[@]} -eq 0 ]; then
+ echo "::error::no fixtures found under protocol/example-*.json"
+ exit 1
+ fi
+ for f in "${fixtures[@]}"; do
+ echo "=== $f ==="
+ npx -y ajv-cli@5 validate \
+ --spec=draft2020 \
+ -s protocol/skill-meta-v1.schema.json \
+ -d "$f"
+ done
+
+ - name: Schema must reject known bad payloads (regression guard)
+ run: |
+ set -euo pipefail
+ tmp=$(mktemp -d)
+ # protocol_version must be 1
+ echo '{"protocol_version":2,"skill_version_latest":"1.0.0","client_detected":"claude","update_doc_url":"https://x"}' > "$tmp/bad-v2.json"
+ # update_command requires update_command_kind (dependentRequired)
+ echo '{"protocol_version":1,"skill_version_latest":"1.0.0","client_detected":"claude","update_doc_url":"https://x","update_command":"echo hi"}' > "$tmp/bad-no-kind.json"
+ # version string must not have a 'v' prefix
+ echo '{"protocol_version":1,"skill_version_latest":"v1.0.0","client_detected":"claude","update_doc_url":"https://x"}' > "$tmp/bad-vprefix.json"
+ # client_detected must be lowercase short identifier
+ echo '{"protocol_version":1,"skill_version_latest":"1.0.0","client_detected":"Claude Desktop","update_doc_url":"https://x"}' > "$tmp/bad-caps.json"
+
+ fail=0
+ for f in "$tmp"/bad-*.json; do
+ if npx -y ajv-cli@5 validate --spec=draft2020 -s protocol/skill-meta-v1.schema.json -d "$f" >/dev/null 2>&1; then
+ echo "::error::$f should have been rejected by the schema but wasn't"
+ fail=1
+ else
+ echo "✓ correctly rejected $(basename "$f")"
+ fi
+ done
+ exit $fail
+
+ spec-cross-references:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Spec doc references all fixtures
+ run: |
+ set -euo pipefail
+ # Every fixture file should be mentioned in the spec's "See also" block,
+ # so adding a new fixture without doc-cross-referencing it fails CI.
+ missing=0
+ for f in protocol/example-*.json; do
+ base=$(basename "$f")
+ if ! grep -q "$base" protocol/skill-meta-v1.md; then
+ echo "::error file=protocol/skill-meta-v1.md::fixture $base is not referenced in the spec"
+ missing=1
+ fi
+ done
+ exit $missing
diff --git a/README.md b/README.md
index 3092540..1ed0110 100644
--- a/README.md
+++ b/README.md
@@ -144,13 +144,38 @@ Just top up. No auto-renewal, no hidden charges.
How do I update?
-**You don't have to — updates are automatic by default.** Your MCP config uses `npx -y @agentkey/mcp`, which re-resolves to the latest published version every time your agent restarts. In Claude Code plugin mode, AgentKey also checks GitHub Releases at runtime and applies a silent in-place update, notifying you:
+There are two pieces and they update differently:
-```
-Claude: AgentKey Skill updated to v1.1.0.
+- **MCP server** (`@agentkey/mcp` npm package): always up to date. Your MCP config runs it as `npx -y @agentkey/mcp`, which re-resolves to the latest published version every time your agent restarts. You never have to touch this.
+
+- **Skill files** (`SKILL.md` + helpers): how this updates depends on your client.
+
+### Claude Code
+
+Updates are automatic. On the first call of a session the skill runs a silent version check; if a new release is available it prompts you to upgrade and (with your consent) runs `npx skills update -g agentkey`.
+
+### Claude Desktop, Cursor, and other clients without an inline Bash tool
+
+The skill cannot run the inline check itself, but starting in v1.4.0 the **MCP server publishes the latest skill version via a dedicated metadata tool (`agentkey_skill_meta`)**. Your agent calls it once per session, compares against this skill's own version, and prompts you to upgrade with the exact command for your client. See [protocol/skill-meta-v1.md](./protocol/skill-meta-v1.md) for the protocol details.
+
+**One-time bootstrap on Desktop:** if you're stuck on a pre-1.4.0 skill in Claude Desktop, the metadata tool exists but your skill rule doesn't know how to read it. Bring yourself current once with:
+
+```bash
+# Replace / with the actual session folder under skills-plugin
+# (usually there's just one; pick the one that contains skills/agentkey/SKILL.md)
+DESKTOP_BASE="$HOME/Library/Application Support/Claude/local-agent-mode-sessions/skills-plugin"
+LATEST_REPO_ZIP=$(mktemp -d)/agentkey.tar.gz
+curl -fsSL https://github.com/chainbase-labs/agentkey/archive/refs/heads/main.tar.gz -o "$LATEST_REPO_ZIP"
+tar -xzf "$LATEST_REPO_ZIP" -C "$(dirname "$LATEST_REPO_ZIP")"
+find "$DESKTOP_BASE" -type d -path "*/skills/agentkey" 2>/dev/null | while read -r dst; do
+ cp -R "$(dirname "$LATEST_REPO_ZIP")"/agentkey-main/skills/agentkey/. "$dst/"
+done
+# Then fully quit and restart Claude Desktop.
```
-**If you'd rather force it manually:**
+After this one bootstrap, future versions will be discovered automatically via the metadata tool.
+
+### Force manual update (any client)
```bash
# Refresh the skill content
@@ -160,6 +185,8 @@ npx skills update agentkey
npx skills add chainbase-labs/agentkey@v1.0.0
```
+Note: `npx skills update` writes to `~/.agents/skills/agentkey` and `~/.claude/skills/agentkey`, which is where Claude Code reads from. **Claude Desktop reads from its own sandbox path** and is not touched by `npx skills update` — use the Desktop bootstrap command above for Desktop.
+
Re-run `npx -y @agentkey/mcp --auth-login` only when you want to rotate your API key.
diff --git a/docs/README_zh.md b/docs/README_zh.md
index 1912b64..923487d 100644
--- a/docs/README_zh.md
+++ b/docs/README_zh.md
@@ -144,13 +144,38 @@ Claude 与 ChatGPT 的原生联网与平台覆盖有限,往往触达不到推
怎么更新?
-**默认不用你管,AgentKey 会自己更新。** 你的 MCP 配置使用的是 `npx -y @agentkey/mcp`,每次 Agent 重启都会自动解析到最新发布版本。Claude Code 插件模式下还会在运行时自动检查 GitHub Release,发现新版本就静默更新并提示:
+AgentKey 有两部分,更新方式不同:
-```
-Claude: AgentKey Skill updated to v1.1.0.
+- **MCP server**(npm 包 `@agentkey/mcp`):永远自动最新。你的 MCP 配置写的是 `npx -y @agentkey/mcp`,每次 Agent 重启都会重新解析到最新发布版。这部分完全不用你管。
+
+- **Skill 文件**(`SKILL.md` 加辅助脚本):升级方式取决于你用的 client。
+
+### Claude Code
+
+完全自动。每次会话第一次调用 skill 时会静默跑版本检查;发现新版本会提示你升级,得到你确认后跑 `npx skills update -g agentkey`。
+
+### Claude Desktop / Cursor 等没有 inline Bash 工具的 client
+
+Skill 自己跑不了 inline 检查,但**从 v1.4.0 起 MCP server 通过专用 metadata tool(`agentkey_skill_meta`)发布最新 skill 版本号**。Agent 在每个会话里调一次,对比本地 skill 版本,发现差异就用你 client 对应的精确命令提示你升级。协议细节见 [protocol/skill-meta-v1.md](../protocol/skill-meta-v1.md)。
+
+**Desktop 一次性破冰升级**:如果你 Desktop 里的 skill 还停在 1.4.0 之前,metadata tool 存在但旧 skill 不懂怎么读。先手动同步一次到最新版:
+
+```bash
+# 把 / 替换成 skills-plugin 下实际的 session 目录
+# (通常就一个,找包含 skills/agentkey/SKILL.md 的那个)
+DESKTOP_BASE="$HOME/Library/Application Support/Claude/local-agent-mode-sessions/skills-plugin"
+LATEST_REPO_ZIP=$(mktemp -d)/agentkey.tar.gz
+curl -fsSL https://github.com/chainbase-labs/agentkey/archive/refs/heads/main.tar.gz -o "$LATEST_REPO_ZIP"
+tar -xzf "$LATEST_REPO_ZIP" -C "$(dirname "$LATEST_REPO_ZIP")"
+find "$DESKTOP_BASE" -type d -path "*/skills/agentkey" 2>/dev/null | while read -r dst; do
+ cp -R "$(dirname "$LATEST_REPO_ZIP")"/agentkey-main/skills/agentkey/. "$dst/"
+done
+# 然后完全退出并重启 Claude Desktop。
```
-**如果你想强制手动更新:**
+破冰之后,后续每次新版都会通过 metadata tool 自动告知,无需再手动操作。
+
+### 任意 client:强制手动更新
```bash
# 拉最新版的 Skill 内容
@@ -160,6 +185,8 @@ npx skills update agentkey
npx skills add chainbase-labs/agentkey@v1.0.0
```
+注意:`npx skills update` 只写 `~/.agents/skills/agentkey` 和 `~/.claude/skills/agentkey` 这两个目录,是 Claude Code 读取的位置。**Claude Desktop 读的是自己的 sandbox 路径**,`npx skills update` 碰不到——Desktop 升级要用上面的破冰命令。
+
只有在需要换 API Key 时才需要再跑一次 `npx -y @agentkey/mcp --auth-login`。
diff --git a/docs/SERVER-IMPLEMENTATION.md b/docs/SERVER-IMPLEMENTATION.md
new file mode 100644
index 0000000..a3ad572
--- /dev/null
+++ b/docs/SERVER-IMPLEMENTATION.md
@@ -0,0 +1,290 @@
+# Server Implementation Guide — `agentkey_skill_meta`
+
+Implementation handoff for the `@agentkey/mcp` MCP server. Tells the server maintainer exactly what to build so the cross-client skill-update path (Claude Desktop, Cursor, etc.) works.
+
+**Authoritative spec**: [protocol/skill-meta-v1.md](../protocol/skill-meta-v1.md). This doc is implementation guidance, not protocol; if it conflicts with the spec, the spec wins.
+
+## What to build
+
+Add one new MCP tool to the server: `agentkey_skill_meta`. It returns a JSON object describing the latest published skill version and how the detected client should upgrade. The skill rule (already in `chainbase-labs/agentkey`) reads this and prompts the user.
+
+That's the entire feature. No new endpoints, no new env vars (except the optional opt-out below), no new dependencies beyond standard `https` / `fs`.
+
+## Component sketch
+
+```
+src/
+├── index.ts # MCP entry; capture clientInfo.name during initialize
+├── tools/
+│ ├── ... (existing tools)
+│ └── skill-meta.ts # NEW — handler for agentkey_skill_meta
+├── lib/
+│ └── github-release-cache.ts # NEW — cached fetch of latest release tag
+└── protocol/
+ └── skill-meta-v1.schema.json # NEW — vendored copy of the spec schema
+```
+
+## Step 1 — Capture `clientInfo` on initialize
+
+In your MCP `initialize` handler, persist `params.clientInfo.name` to a module-level variable (or whichever request-scoped storage your server uses). The handler runs once per connection; subsequent tool calls read this value.
+
+```ts
+// src/index.ts (sketch)
+let clientName = "unknown";
+
+server.setRequestHandler(InitializeRequestSchema, async (req) => {
+ clientName = req.params.clientInfo?.name ?? "unknown";
+ // ... return capabilities
+});
+
+export const getClientName = () => clientName;
+```
+
+If your server is already multi-tenant or runs as a daemon serving many MCP sessions, store this per-connection rather than module-level.
+
+## Step 2 — Cached release-tag fetch
+
+GitHub API: `GET https://api.github.com/repos/chainbase-labs/agentkey/releases/latest`. Cache for **24 h** in `${XDG_CACHE_HOME:-$HOME/.cache}/agentkey/skill-version.json` (Windows: `%LOCALAPPDATA%\agentkey\skill-version.json`).
+
+```ts
+// src/lib/github-release-cache.ts (sketch)
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { homedir } from "node:os";
+
+interface Cache {
+ tag: string;
+ fetched_at: number; // epoch ms
+ etag?: string;
+}
+
+const CACHE_PATH = join(
+ process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"),
+ "agentkey",
+ "skill-version.json"
+);
+const TTL_MS = 24 * 60 * 60 * 1000;
+
+let inFlight: Promise | null = null;
+
+export async function getLatestSkillVersion(): Promise {
+ if (inFlight) return inFlight;
+ inFlight = (async () => {
+ try {
+ const cached = await readCache();
+ if (cached && Date.now() - cached.fetched_at < TTL_MS) return cached.tag;
+ const fresh = await fetchFromGitHub(cached?.etag);
+ if (fresh) await writeCache(fresh);
+ return fresh?.tag ?? cached?.tag ?? "";
+ } catch {
+ return ""; // network/parse failure → skill rule treats as "unknown"
+ } finally {
+ inFlight = null;
+ }
+ })();
+ return inFlight;
+}
+
+async function fetchFromGitHub(prevEtag?: string): Promise {
+ const res = await fetch(
+ "https://api.github.com/repos/chainbase-labs/agentkey/releases/latest",
+ {
+ headers: {
+ "User-Agent": "@agentkey/mcp",
+ ...(prevEtag ? { "If-None-Match": prevEtag } : {}),
+ },
+ signal: AbortSignal.timeout(3000),
+ }
+ );
+ if (res.status === 304) return null; // not modified
+ if (!res.ok) return null; // 403 rate limit, 5xx, etc.
+ const body = (await res.json()) as { tag_name?: string };
+ if (!body.tag_name) return null;
+ return {
+ tag: body.tag_name.replace(/^v/, ""),
+ fetched_at: Date.now(),
+ etag: res.headers.get("etag") ?? undefined,
+ };
+}
+
+async function readCache(): Promise {
+ try {
+ const raw = await readFile(CACHE_PATH, "utf8");
+ return JSON.parse(raw);
+ } catch {
+ return null;
+ }
+}
+
+async function writeCache(c: Cache): Promise {
+ await mkdir(dirname(CACHE_PATH), { recursive: true });
+ await writeFile(CACHE_PATH, JSON.stringify(c), "utf8");
+}
+```
+
+Why a 3-second timeout: the tool MUST respond fast enough not to block `list_tools` discovery. A 24 h cache means at most one network call per day per machine, and a stale cache is always preferred over a slow response.
+
+## Step 3 — Client → upgrade-recipe map
+
+```ts
+// src/tools/skill-meta.ts (sketch)
+type Recipe = { command: string; kind: "shell" | "manual_ui" };
+
+const RECIPES: Record = {
+ "claude-code": { command: "npx -y skills update -g agentkey", kind: "shell" },
+ "claude": { command: "bash <(curl -fsSL https://agentkey.app/update-desktop.sh)", kind: "shell" },
+ "cursor": { command: "npx -y skills update -g agentkey", kind: "shell" },
+ "codex": { command: "npx -y skills update -g agentkey", kind: "shell" },
+};
+
+function normalizeClient(raw: string): string {
+ const s = raw.toLowerCase().trim();
+ if (s.includes("claude code")) return "claude-code";
+ if (s.includes("claude")) return "claude";
+ if (s.includes("cursor")) return "cursor";
+ if (s.includes("codex")) return "codex";
+ if (s.includes("cline")) return "cline";
+ if (s.includes("windsurf")) return "windsurf";
+ if (s.includes("continue")) return "continue";
+ return "unknown";
+}
+```
+
+The Desktop one-liner currently points at a `bash <(curl ...)` because the only reliable way to find Desktop's sandbox path is to inspect `~/Library/Application Support/Claude/...` at runtime. That script is owned by the agentkey.app maintainers; until it ships, emit `update_command_kind: "manual_ui"` with the README URL instead.
+
+## Step 4 — The tool itself
+
+```ts
+// src/tools/skill-meta.ts
+import { getLatestSkillVersion } from "../lib/github-release-cache.js";
+import { getClientName } from "../index.js";
+
+export const SKILL_META_TOOL = {
+ name: "agentkey_skill_meta",
+ description:
+ "Internal AgentKey skill metadata. Call once at session start with `{}` to retrieve the latest skill version and client-specific upgrade instructions. The response is non-actionable metadata; do not surface its raw JSON to the user. Compare `skill_version_latest` against this skill's `version:` frontmatter and follow `update_command` / `update_doc_url` if they differ.",
+ inputSchema: {
+ type: "object",
+ properties: {},
+ additionalProperties: false,
+ },
+} as const;
+
+export async function handleSkillMeta() {
+ if (process.env.AGENTKEY_NO_VERSION_BEACON === "1") {
+ // user opted out — still return a minimally valid response
+ return {
+ protocol_version: 1 as const,
+ skill_version_latest: "",
+ client_detected: normalizeClient(getClientName()),
+ update_doc_url: "https://agentkey.app/docs/upgrade",
+ };
+ }
+ const latest = await getLatestSkillVersion(); // never throws; "" on failure
+ const client = normalizeClient(getClientName());
+ const recipe = RECIPES[client];
+ return {
+ protocol_version: 1 as const,
+ skill_version_latest: latest,
+ client_detected: client,
+ update_doc_url: "https://agentkey.app/docs/upgrade",
+ ...(recipe ? { update_command: recipe.command, update_command_kind: recipe.kind } : {}),
+ ...(latest ? { release_notes_url: `https://github.com/chainbase-labs/agentkey/releases/tag/v${latest}` } : {}),
+ };
+}
+```
+
+Register `SKILL_META_TOOL` in your `list_tools` handler, and route invocations of `agentkey_skill_meta` to `handleSkillMeta`. The MCP `CallToolResult` should wrap the JSON in a `content[0]` text block: `{ content: [{ type: "text", text: JSON.stringify(response) }] }`.
+
+## Step 5 — Vendor the schema + CI validation
+
+Copy `protocol/skill-meta-v1.schema.json` from this repo into the server repo at `protocol/skill-meta-v1.schema.json`. Validate every emitted response against it before returning:
+
+```ts
+import Ajv from "ajv";
+import schema from "../protocol/skill-meta-v1.schema.json" with { type: "json" };
+
+const ajv = new Ajv();
+const validate = ajv.compile(schema);
+
+export async function handleSkillMeta() {
+ const response = /* ... as above ... */;
+ if (!validate(response)) {
+ // schema bug; fail loudly in dev, but DO NOT throw at runtime — emit
+ // a minimum-viable v1 response so the agent's list_tools doesn't break
+ console.error("[skill-meta] response failed schema:", validate.errors);
+ return {
+ protocol_version: 1 as const,
+ skill_version_latest: "",
+ client_detected: "unknown",
+ update_doc_url: "https://agentkey.app/docs/upgrade",
+ };
+ }
+ return response;
+}
+```
+
+Add a CI workflow on the server side that diffs the vendored schema against this repo's authoritative copy:
+
+```yaml
+# .github/workflows/protocol-drift.yml (in @agentkey/mcp repo)
+on:
+ pull_request:
+ schedule: [{cron: '0 12 * * 1'}]
+jobs:
+ drift:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Fetch upstream schema
+ run: curl -fsSL https://raw.githubusercontent.com/chainbase-labs/agentkey/main/protocol/skill-meta-v1.schema.json > /tmp/upstream.json
+ - name: Diff against vendored copy
+ run: diff /tmp/upstream.json protocol/skill-meta-v1.schema.json
+```
+
+CI failure on `diff` is the signal: "upstream protocol changed, sync your vendored copy (and update the implementation if a new field was added)".
+
+## Required tests
+
+Three categories — all should exist in the server repo's test suite before shipping:
+
+1. **Schema conformance** (per fixture). For each of the four `protocol/example-response-*.json` fixtures in this repo, your `handleSkillMeta` should be able to produce a response matching one of them (modulo dynamic fields like `release_notes_url`).
+
+2. **Failure modes**. Mock the GitHub API to return:
+ - 200 with a valid tag → response has `skill_version_latest` set
+ - 403 (rate limit) → response has `skill_version_latest: ""`
+ - Network error → response has `skill_version_latest: ""`
+ - 200 with malformed JSON → response has `skill_version_latest: ""`
+ - All four → `validate(response) === true`
+
+3. **Client detection**. For each known `clientInfo.name` ("Claude", "Claude Code", "Cursor", "Codex", "Anthropic Computer Use Demo", ""), the normalized `client_detected` matches the spec table, and the recipe map either provides a command or is absent.
+
+## Performance budget
+
+- `list_tools` exposing the new tool: +1 entry, no extra latency
+- First call to `agentkey_skill_meta` with cold cache: ≤ 3 s (network), then cached
+- Subsequent calls: ≤ 10 ms (file read + JSON parse)
+- Memory: < 1 KB cached, no goroutines / timers needed
+
+## Opt-out
+
+Honor the env var `AGENTKEY_NO_VERSION_BEACON=1`: tool stays registered (so the skill rule doesn't fall through to legacy bash), but emits a minimum-viable response with empty `skill_version_latest`. The skill rule then skips the version comparison silently.
+
+## What NOT to do
+
+| Anti-pattern | Why not |
+|---|---|
+| Throw on network failure | Crashes `list_tools` on some clients; user sees broken MCP server |
+| Skip registering the tool when cache is empty | Skill rule then falls through to inline-bash path on Desktop, which doesn't work — defeats the entire feature |
+| Add the version string to every tool's `description` as a side channel | We considered it as a transition mechanism for old skills, but it pollutes prompt context with every `list_tools` call and is hard to retire. Keep the channel single-purpose |
+| Auto-execute the upgrade from inside the server | Cross-process writes to a client's sandbox directory; sandbox path changes break us; bad debuggability. Notify + instruct, don't auto-mutate |
+| Skip the `update_doc_url` field | It's the only field guaranteed to exist across all protocol versions. Skill rules that don't understand future fields fall back to it. Without it they have nothing to show the user |
+
+## Release coordination with this repo
+
+1. Implement and merge in `@agentkey/mcp`
+2. `npm publish` a new version
+3. (Verify) Any user with `npx -y @agentkey/mcp` in their config will pick it up on next agent restart automatically
+4. In this repo, a new skill release (`v1.4.0`) ships the SKILL.md rule that reads the metadata tool
+5. Existing skill versions (≤1.3.x) silently ignore the new tool — no regression; they continue to use the inline bash path on Claude Code and have no upgrade path on Desktop (status quo)
+6. New skill versions (≥1.4.0) work everywhere
diff --git a/protocol/example-response-claude-code.json b/protocol/example-response-claude-code.json
new file mode 100644
index 0000000..ea2e032
--- /dev/null
+++ b/protocol/example-response-claude-code.json
@@ -0,0 +1,9 @@
+{
+ "protocol_version": 1,
+ "skill_version_latest": "1.3.0",
+ "client_detected": "claude-code",
+ "update_doc_url": "https://agentkey.app/docs/upgrade",
+ "update_command": "npx -y skills update -g agentkey",
+ "update_command_kind": "shell",
+ "release_notes_url": "https://github.com/chainbase-labs/agentkey/releases/tag/v1.3.0"
+}
diff --git a/protocol/example-response-claude-desktop.json b/protocol/example-response-claude-desktop.json
new file mode 100644
index 0000000..ad0cffa
--- /dev/null
+++ b/protocol/example-response-claude-desktop.json
@@ -0,0 +1,9 @@
+{
+ "protocol_version": 1,
+ "skill_version_latest": "1.3.0",
+ "client_detected": "claude",
+ "update_doc_url": "https://agentkey.app/docs/upgrade",
+ "update_command": "bash <(curl -fsSL https://agentkey.app/update-desktop.sh)",
+ "update_command_kind": "shell",
+ "release_notes_url": "https://github.com/chainbase-labs/agentkey/releases/tag/v1.3.0"
+}
diff --git a/protocol/example-response-offline.json b/protocol/example-response-offline.json
new file mode 100644
index 0000000..7daf99f
--- /dev/null
+++ b/protocol/example-response-offline.json
@@ -0,0 +1,6 @@
+{
+ "protocol_version": 1,
+ "skill_version_latest": "",
+ "client_detected": "claude-code",
+ "update_doc_url": "https://agentkey.app/docs/upgrade"
+}
diff --git a/protocol/example-response-unknown-client.json b/protocol/example-response-unknown-client.json
new file mode 100644
index 0000000..c6ec426
--- /dev/null
+++ b/protocol/example-response-unknown-client.json
@@ -0,0 +1,6 @@
+{
+ "protocol_version": 1,
+ "skill_version_latest": "1.3.0",
+ "client_detected": "unknown",
+ "update_doc_url": "https://agentkey.app/docs/upgrade"
+}
diff --git a/protocol/skill-meta-v1.md b/protocol/skill-meta-v1.md
new file mode 100644
index 0000000..0cbf91c
--- /dev/null
+++ b/protocol/skill-meta-v1.md
@@ -0,0 +1,174 @@
+# AgentKey Skill-Meta Protocol v1
+
+Contract between **`@agentkey/mcp`** (server, npm package) and **`chainbase-labs/agentkey`** (this skill repo). The server publishes the skill's latest version + client-specific upgrade instructions via a dedicated MCP tool; the skill (via the agent) reads it and tells the user how to upgrade.
+
+This protocol exists because some MCP clients — notably Claude Desktop — cannot execute the inline `bash` block in `SKILL.md` Step 0, so the in-skill update-check path silently fails there. Routing the check through the always-on MCP server makes upgrades discoverable on every client.
+
+## Tool contract
+
+The server MUST expose a tool named exactly `agentkey_skill_meta` via `list_tools`. The tool MUST:
+
+- Take **no required parameters** (an empty `{}` input is valid)
+- Be safe to call repeatedly (idempotent, no side effects)
+- Return a JSON object conforming to `SkillMetaResponse` (see schema)
+- Respond in **under 200 ms in the steady state** (use a cached GitHub Releases lookup)
+- Never throw on network failure — fall back gracefully (see §Failure modes)
+
+The tool's `description` in `list_tools` MUST instruct the agent to call it **once per session, before any business tool call**, and MUST NOT make the agent believe it has business value (it is purely metadata).
+
+Suggested description:
+
+> Internal AgentKey skill metadata. Call once at session start with `{}` to retrieve the latest skill version and client-specific upgrade instructions. The response is non-actionable metadata; do not surface its raw JSON to the user. Compare `skill_version_latest` against this skill's `version:` frontmatter and follow `update_command` / `update_doc_url` if they differ.
+
+## Response shape (v1)
+
+```ts
+interface SkillMetaResponse {
+ /** Protocol version. Always 1 in this spec. Bumped only for breaking changes. */
+ protocol_version: 1;
+
+ /** Latest published skill release tag, without 'v' prefix. e.g. "1.3.0".
+ * Empty string allowed only when the server cannot reach GitHub (see Failure modes). */
+ skill_version_latest: string;
+
+ /** Lowercase short name of the MCP client that called this tool.
+ * Derived from MCP `initialize`'s `clientInfo.name`. Examples:
+ * "claude" (Claude Desktop), "claude-code", "cursor", "codex", "unknown".
+ * Servers MUST emit "unknown" rather than throwing if clientInfo is absent. */
+ client_detected: string;
+
+ /** Stable upgrade documentation URL. MUST be present in EVERY response, for EVERY
+ * client, EVERY protocol version. This is the bottom-of-the-barrel fallback the
+ * skill rule can always recommend if it doesn't understand anything else. */
+ update_doc_url: string;
+
+ /** Optional. Concrete one-line upgrade instruction for this client.
+ * - When kind="shell": a verbatim shell command the user runs in a terminal
+ * - When kind="manual_ui": a short instruction like "Settings → Capabilities → Skills → reinstall"
+ * Servers SHOULD include this whenever they have a known recipe for the detected client. */
+ update_command?: string;
+
+ /** Optional. Indicates how to interpret `update_command`. */
+ update_command_kind?: "shell" | "manual_ui";
+
+ /** Optional. URL to the human-readable release notes for skill_version_latest.
+ * Typically the GitHub Release page. */
+ release_notes_url?: string;
+}
+```
+
+The wire JSON Schema is in [skill-meta-v1.schema.json](./skill-meta-v1.schema.json). The TypeScript interface above is normative for human readers; the JSON Schema is normative for CI validation.
+
+### Required vs. optional — and why
+
+Five guarantees the skill rule depends on across all v1 servers:
+
+1. `protocol_version === 1` (router)
+2. `skill_version_latest` is a string
+3. `client_detected` is a string
+4. `update_doc_url` is a string (fallback that always works)
+5. Adding new optional fields MUST NOT bump `protocol_version`
+
+If you cannot guarantee #1–#4 in your implementation, you are not v1-compliant; emit `protocol_version: 0` (reserved) or omit the tool entirely.
+
+## Client identifier conventions
+
+Servers SHOULD map MCP `clientInfo.name` to lowercase short names:
+
+| `clientInfo.name` substring (case-insensitive) | `client_detected` value |
+|---|---|
+| `claude code` | `claude-code` |
+| `claude` (no "code") | `claude` |
+| `cursor` | `cursor` |
+| `codex` | `codex` |
+| `cline` | `cline` |
+| `windsurf` | `windsurf` |
+| `continue` | `continue` |
+| anything else | `unknown` |
+
+The list grows over time; adding a new client to the map is a non-breaking change.
+
+## Server behavior
+
+### Caching
+
+The server MUST cache the GitHub Releases lookup. Recommended:
+
+- TTL: 24 h
+- Cache path: `${XDG_CACHE_HOME:-$HOME/.cache}/agentkey/skill-version.json` (Linux/macOS) or `%LOCALAPPDATA%\agentkey\skill-version.json` (Windows)
+- Concurrent requests: deduplicate (one in-flight fetch per process)
+- Cache structure: `{ tag: string, fetched_at: number, etag?: string }` — the optional `etag` lets the next refresh do a conditional `GET` and avoid rate limit cost
+
+### Failure modes
+
+| Failure | Behavior |
+|---|---|
+| First fetch, no network | Return `skill_version_latest: ""`, omit `update_command` and `release_notes_url`, still include `update_doc_url`. The skill rule treats empty `skill_version_latest` as "unknown, skip the check". |
+| GitHub rate limit (HTTP 403) | Same as above. |
+| Cache file corrupted | Delete it and refetch; if that also fails, return empty `skill_version_latest`. |
+| `clientInfo` missing from `initialize` | Set `client_detected: "unknown"` and omit `update_command`. Still emit valid response. |
+
+The tool MUST NOT throw under any of the above; throwing would crash the agent's `list_tools` enumeration on some clients.
+
+### Update command recipes (recommended baseline)
+
+| `client_detected` | `update_command_kind` | `update_command` |
+|---|---|---|
+| `claude-code` | `shell` | `npx -y skills update -g agentkey` |
+| `claude` | `shell` | `bash <(curl -fsSL https://agentkey.app/update-desktop.sh)` |
+| `cursor` | `shell` | `npx -y skills update -g agentkey` |
+| `codex` | `shell` | `npx -y skills update -g agentkey` |
+| `unknown` | (omit) | (omit) — agent falls back to `update_doc_url` |
+
+The Desktop one-liner is a server-side responsibility; the script it points at is responsible for locating the Desktop sandbox path and writing the new skill files. It is NOT this repo's responsibility, but the URL MUST be live before `update_command_kind: "shell"` is emitted for `client: "claude"`.
+
+## Skill behavior (this repo)
+
+The SKILL.md rule MUST:
+
+1. At session start, before any business tool call, call `agentkey_skill_meta` once with `{}`
+2. If the tool is not in `list_tools`, skip silently (server is pre-v1; fall back to the legacy Step-0 inline bash check)
+3. If the call fails (timeout, exception, malformed JSON), skip silently
+4. If `response.protocol_version !== 1`, only honor `update_doc_url`; ignore everything else
+5. If `response.skill_version_latest === ""`, skip the comparison (server admitted it doesn't know)
+6. Compare `response.skill_version_latest` to this skill's `version:` frontmatter (semver string compare; if they differ → prompt user)
+7. When prompting, prefer `update_command` (display verbatim, do not modify); fall back to `update_doc_url` only if no command available
+8. Never surface raw response JSON to the user
+
+The rule MUST NOT:
+
+- Call `agentkey_skill_meta` more than once per session
+- Mutate the response or rewrite it as a different shell command
+- Block the user's actual request waiting for the update (prompt once, then proceed)
+
+## Versioning
+
+This is `v1`. The protocol uses **additive evolution**:
+
+- **Allowed without bumping protocol_version**: adding optional fields, adding new `client_detected` enum values, adding new `update_command_kind` enum values (skill rule treats unknown kinds as `manual_ui`)
+- **Requires `protocol_version: 2`**: renaming a required field, changing a required field's type, removing a required field, changing the semantics of `update_command`
+
+When v2 ships:
+
+- Server SHOULD emit both responses when possible (e.g. via the v1 tool always returning v1 shape, and a new `agentkey_skill_meta_v2` tool returning v2)
+- Or: server emits only v2 but ensures the v1-required fields above are still present (graceful enough for v1 skills to read `update_doc_url`)
+- v1 skill rule sees `protocol_version: 2` → falls back to `update_doc_url` (rule 4 above)
+
+This means **v1 skills are never broken by future server upgrades**, regardless of what v2/v3/... add. The cost of that guarantee is the five immortal fields in §Required vs. optional.
+
+## Single source of truth
+
+The schema lives **only here** (`protocol/skill-meta-v1.schema.json`). Server implementations MUST consume this schema, either:
+
+- Vendor it at build time: `curl https://raw.githubusercontent.com/chainbase-labs/agentkey/main/protocol/skill-meta-v1.schema.json > schema/skill-meta-v1.schema.json` and commit
+- Or fetch on CI and `diff` against the vendored copy — CI fail forces a sync PR
+
+The server's own CI MUST validate every `SkillMetaResponse` it emits against this schema before responding. The skill repo's CI validates `protocol/example-*.json` fixtures against the schema. Neither side rewrites the schema unilaterally; changes are PRs against this file.
+
+## See also
+
+- [example-response-claude-desktop.json](./example-response-claude-desktop.json) — fixture for Desktop client
+- [example-response-claude-code.json](./example-response-claude-code.json) — fixture for Code client
+- [example-response-unknown-client.json](./example-response-unknown-client.json) — fixture for unrecognized client
+- [example-response-offline.json](./example-response-offline.json) — fixture for the server-offline / rate-limited failure mode
+- [docs/SERVER-IMPLEMENTATION.md](../docs/SERVER-IMPLEMENTATION.md) — implementation guide for `@agentkey/mcp` maintainers
diff --git a/protocol/skill-meta-v1.schema.json b/protocol/skill-meta-v1.schema.json
new file mode 100644
index 0000000..2724c81
--- /dev/null
+++ b/protocol/skill-meta-v1.schema.json
@@ -0,0 +1,55 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://raw.githubusercontent.com/chainbase-labs/agentkey/main/protocol/skill-meta-v1.schema.json",
+ "title": "SkillMetaResponse v1",
+ "description": "Wire format for the agentkey_skill_meta MCP tool. See protocol/skill-meta-v1.md for the full contract.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "protocol_version",
+ "skill_version_latest",
+ "client_detected",
+ "update_doc_url"
+ ],
+ "properties": {
+ "protocol_version": {
+ "description": "Protocol version. v1 servers MUST emit literal 1.",
+ "const": 1
+ },
+ "skill_version_latest": {
+ "description": "Latest published skill release, semver without 'v' prefix. Empty string is allowed only when the server cannot determine the latest version (e.g. offline, GitHub rate-limited).",
+ "type": "string",
+ "pattern": "^$|^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$"
+ },
+ "client_detected": {
+ "description": "Lowercase short name of the MCP client derived from initialize.clientInfo.name. Use 'unknown' when clientInfo is absent or unrecognized.",
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
+ },
+ "update_doc_url": {
+ "description": "Stable documentation URL describing how to upgrade. MUST be present in every response, regardless of client.",
+ "type": "string",
+ "pattern": "^https?://"
+ },
+ "update_command": {
+ "description": "Optional. Concrete one-line upgrade instruction for the detected client. When present, update_command_kind MUST also be present.",
+ "type": "string",
+ "minLength": 1
+ },
+ "update_command_kind": {
+ "description": "Optional. Indicates how to interpret update_command. Skill rules treat unknown future kinds as 'manual_ui'.",
+ "type": "string",
+ "enum": ["shell", "manual_ui"]
+ },
+ "release_notes_url": {
+ "description": "Optional. URL of the human-readable release notes for skill_version_latest.",
+ "type": "string",
+ "pattern": "^https?://"
+ }
+ },
+ "dependentRequired": {
+ "update_command": ["update_command_kind"],
+ "update_command_kind": ["update_command"]
+ }
+}
diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md
index 59455fb..0a17911 100644
--- a/skills/agentkey/SKILL.md
+++ b/skills/agentkey/SKILL.md
@@ -22,24 +22,44 @@ license: MIT
**Step 0 (always run first):**
-1. Run the version check silently (cached — repeat calls are <10ms):
- ```bash
- bash "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/skills/agentkey/scripts/check-update.sh" 2>/dev/null
- ```
- - `UP_TO_DATE` or empty → continue silently to step 2.
- - `UPGRADE_AVAILABLE ` → run the **Upgrade flow** below, then continue to step 2.
+0.A — **Server beacon check (cross-client; runs on every client including Claude Desktop):**
+
+If the tool `agentkey_skill_meta` is visible in `list_tools`, call it **once** with `{}`. Otherwise skip to 0.B.
+
+The response is structured JSON conforming to [protocol/skill-meta-v1](https://github.com/chainbase-labs/agentkey/blob/main/protocol/skill-meta-v1.md). Apply these rules **in order**:
+
+1. Response missing, threw, or not valid JSON → skip silently to 0.B.
+2. `response.protocol_version !== 1` → only honor `update_doc_url`; if present, remember it as a fallback for any prompt below.
+3. `response.skill_version_latest === ""` → server admitted it can't tell (offline / rate-limited). Skip to 0.B without prompting.
+4. `response.skill_version_latest` equals this SKILL.md's frontmatter `version:` field (read it from the top of this file) → up to date. Skip to 0.B.
+5. Otherwise (versions differ): run the **Upgrade flow** below using `response.update_command` / `response.update_command_kind` / `response.update_doc_url`. **Never** call `agentkey_skill_meta` a second time in the same session.
-2. Confirm the 4 MCP tools — `list_tools`, `find_tools`, `describe_tool`, `execute_tool` — are visible in the current toolset. If **any** are missing → **Setup** (regardless of what the user asked). Do not attempt Query without all 4.
+0.B — **Inline check (compatibility path for clients with a Bash tool, e.g. Claude Code):**
+
+```bash
+bash "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/skills/agentkey/scripts/check-update.sh" 2>/dev/null
+```
+
+- `UP_TO_DATE` or empty → continue silently to 0.C.
+- `UPGRADE_AVAILABLE ` → if 0.A already prompted the user this session, skip; else run the legacy **Upgrade flow** below with the new version, then continue to 0.C.
+
+If your client has no Bash tool (Claude Desktop, some web-based clients), this step is a no-op — that's fine, 0.A already covered it.
+
+0.C — **Verify MCP tools.** Confirm `list_tools`, `find_tools`, `describe_tool`, `execute_tool` are visible. If **any** are missing → **Setup** (regardless of what the user asked). Do not attempt Query without all 4.
### Upgrade flow
-Triggered when `check-update.sh` outputs `UPGRADE_AVAILABLE `. Substitute `` and `` with the actual versions parsed from that line.
+Triggered by either:
+- **(A)** Step 0.A: `agentkey_skill_meta` returned a `skill_version_latest` different from this SKILL.md's frontmatter version. Use that response's `update_command` (when present) instead of the default `npx skills update` command below. The `` is this SKILL.md's frontmatter version; `` is `response.skill_version_latest`.
+- **(B)** Step 0.B: `check-update.sh` printed `UPGRADE_AVAILABLE `. Use `` and `` from that line.
+
+Below, `` and `` refer to whichever pair was resolved above.
**Step A — Check for auto-upgrade opt-in.** Run:
```bash
if [ "${AGENTKEY_AUTO_UPGRADE:-0}" = "1" ] || [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/auto-upgrade" ]; then echo AUTO=1; fi
```
-If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\ → v\…", run **Step C**, then continue to step 2. **Do not** show the AskUserQuestion prompt.
+If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\ → v\…", run **Step C**, then continue to step 0.C. **Do not** show the AskUserQuestion prompt.
**Step B — Otherwise, prompt the user with AskUserQuestion:**
- Question: `AgentKey v is available (currently on v). Upgrade now?`
@@ -65,18 +85,24 @@ If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\
mkdir -p "$_CFG" && echo "$_NEW $_LEVEL $(date +%s)" > "$_SNOOZE"
echo "SNOOZED_LEVEL=$_LEVEL"
```
- Translate the level into a duration for the user — `SNOOZED_LEVEL=1` → "Next reminder in 24h", `2` → "in 48h", `3` → "in 1 week". Continue to step 2 — **do not** upgrade.
+ Translate the level into a duration for the user — `SNOOZED_LEVEL=1` → "Next reminder in 24h", `2` → "in 48h", `3` → "in 1 week". Continue to step 0.C — **do not** upgrade.
- **`Never ask again`** → run:
```bash
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" && touch "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/update-disabled"
```
- Tell the user "Update checks disabled. Remove `~/.config/agentkey/update-disabled` to re-enable." Continue to step 2 — **do not** upgrade.
+ Tell the user "Update checks disabled. Remove `~/.config/agentkey/update-disabled` to re-enable." Continue to step 0.C — **do not** upgrade.
+
+**Step C — Run the upgrade.**
+
+If trigger was (A) and `response.update_command` is present:
+- `update_command_kind === "shell"` → Display the command verbatim and offer to run it in Bash if a Bash tool is available. If no Bash tool (e.g. Claude Desktop), instruct the user to copy-paste it into their terminal.
+- `update_command_kind === "manual_ui"` (or anything else) → Display `response.update_command` as instructions; do **not** attempt to execute. Always also offer `response.update_doc_url` as the doc link.
-**Step C — Run the upgrade.** Invoke:
+If trigger was (B), or trigger (A) had no `update_command` (only `update_doc_url`), fall back to the default Claude-Code-friendly path:
```bash
npx skills update agentkey
```
-On success: tell the user "✓ AgentKey updated to v\." On failure: show the failure verbatim and tell the user "Run `npx skills update agentkey` manually to retry." Either way, continue to step 2.
+On success: tell the user "✓ AgentKey updated to v\." On failure: show the failure verbatim and tell the user "Run `npx skills update agentkey` manually to retry." Either way, continue to step 0.C.
Then route by intent:
- "setup"/"install"/"api key"/"reinstall" → **Setup**
From c9034e1a9d442ea11a72b54d184c9c1958ee8080 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?=
<31078449+Nowhitestar@users.noreply.github.com>
Date: Tue, 12 May 2026 16:27:09 +0800
Subject: [PATCH 3/3] fix(skill): honest no-Bash fallback and GitHub-release
manual path
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two correctness fixes to the v1.4.0 beacon design before shipping:
1. Step B no-Bash fallback. The four AskUserQuestion options each persist
state by writing files under ~/.config/agentkey/. On Claude Desktop and
other clients without a Bash tool, those writes silently fail today,
so the rule would tell the user "snooze enabled" or "auto-upgrade on"
while nothing was actually saved. Each option now branches on whether
Bash is available; the no-Bash branch tells the user what didn't get
saved and gives them the exact one-line terminal command to persist
the setting manually.
2. Step C fallback path. Previously, when no shell update_command was
available the rule pointed users at update_doc_url, which the server
defaulted to https://agentkey.app/docs/upgrade — a URL we don't have.
Now both the schema fixtures and the SKILL.md fallback point to
https://github.com/chainbase-labs/agentkey/releases/latest, which is
the actual authoritative source for release archives.
Knock-on changes:
- example-response-claude-desktop.json no longer carries a stub shell
command for a script that doesn't exist; Desktop deliberately falls
through to the GitHub-release manual path until a real installer ships.
- Server recipe map in docs/SERVER-IMPLEMENTATION.md drops the Desktop
entry, with a comment explaining when to add it back.
- Spec table marks Desktop as (omit) with the rationale captured inline.
---
docs/SERVER-IMPLEMENTATION.md | 13 ++--
protocol/example-response-claude-code.json | 2 +-
protocol/example-response-claude-desktop.json | 4 +-
protocol/example-response-offline.json | 2 +-
protocol/example-response-unknown-client.json | 2 +-
protocol/skill-meta-v1.md | 6 +-
skills/agentkey/SKILL.md | 71 ++++++++++---------
7 files changed, 54 insertions(+), 46 deletions(-)
diff --git a/docs/SERVER-IMPLEMENTATION.md b/docs/SERVER-IMPLEMENTATION.md
index a3ad572..85a2761 100644
--- a/docs/SERVER-IMPLEMENTATION.md
+++ b/docs/SERVER-IMPLEMENTATION.md
@@ -132,9 +132,12 @@ type Recipe = { command: string; kind: "shell" | "manual_ui" };
const RECIPES: Record = {
"claude-code": { command: "npx -y skills update -g agentkey", kind: "shell" },
- "claude": { command: "bash <(curl -fsSL https://agentkey.app/update-desktop.sh)", kind: "shell" },
"cursor": { command: "npx -y skills update -g agentkey", kind: "shell" },
"codex": { command: "npx -y skills update -g agentkey", kind: "shell" },
+ // "claude" (Desktop) deliberately omitted — Desktop's sandbox skill path
+ // isn't reachable by `npx skills update`, and there's no first-party
+ // installer script yet. The skill rule falls back to update_doc_url
+ // (= the GitHub releases page) and instructs the user to download manually.
};
function normalizeClient(raw: string): string {
@@ -150,7 +153,7 @@ function normalizeClient(raw: string): string {
}
```
-The Desktop one-liner currently points at a `bash <(curl ...)` because the only reliable way to find Desktop's sandbox path is to inspect `~/Library/Application Support/Claude/...` at runtime. That script is owned by the agentkey.app maintainers; until it ships, emit `update_command_kind: "manual_ui"` with the README URL instead.
+There is intentionally no Desktop recipe yet. The skill rule (Step C, branch A) handles a missing `update_command` by telling the user to download the latest release from GitHub manually. Adding a Desktop one-liner later is a non-breaking change — just add the row to `RECIPES` and ship a new server version; no protocol bump and no skill change needed.
## Step 4 — The tool itself
@@ -177,7 +180,7 @@ export async function handleSkillMeta() {
protocol_version: 1 as const,
skill_version_latest: "",
client_detected: normalizeClient(getClientName()),
- update_doc_url: "https://agentkey.app/docs/upgrade",
+ update_doc_url: "https://github.com/chainbase-labs/agentkey/releases/latest",
};
}
const latest = await getLatestSkillVersion(); // never throws; "" on failure
@@ -187,7 +190,7 @@ export async function handleSkillMeta() {
protocol_version: 1 as const,
skill_version_latest: latest,
client_detected: client,
- update_doc_url: "https://agentkey.app/docs/upgrade",
+ update_doc_url: "https://github.com/chainbase-labs/agentkey/releases/latest",
...(recipe ? { update_command: recipe.command, update_command_kind: recipe.kind } : {}),
...(latest ? { release_notes_url: `https://github.com/chainbase-labs/agentkey/releases/tag/v${latest}` } : {}),
};
@@ -217,7 +220,7 @@ export async function handleSkillMeta() {
protocol_version: 1 as const,
skill_version_latest: "",
client_detected: "unknown",
- update_doc_url: "https://agentkey.app/docs/upgrade",
+ update_doc_url: "https://github.com/chainbase-labs/agentkey/releases/latest",
};
}
return response;
diff --git a/protocol/example-response-claude-code.json b/protocol/example-response-claude-code.json
index ea2e032..65dd829 100644
--- a/protocol/example-response-claude-code.json
+++ b/protocol/example-response-claude-code.json
@@ -2,7 +2,7 @@
"protocol_version": 1,
"skill_version_latest": "1.3.0",
"client_detected": "claude-code",
- "update_doc_url": "https://agentkey.app/docs/upgrade",
+ "update_doc_url": "https://github.com/chainbase-labs/agentkey/releases/latest",
"update_command": "npx -y skills update -g agentkey",
"update_command_kind": "shell",
"release_notes_url": "https://github.com/chainbase-labs/agentkey/releases/tag/v1.3.0"
diff --git a/protocol/example-response-claude-desktop.json b/protocol/example-response-claude-desktop.json
index ad0cffa..4f84f00 100644
--- a/protocol/example-response-claude-desktop.json
+++ b/protocol/example-response-claude-desktop.json
@@ -2,8 +2,6 @@
"protocol_version": 1,
"skill_version_latest": "1.3.0",
"client_detected": "claude",
- "update_doc_url": "https://agentkey.app/docs/upgrade",
- "update_command": "bash <(curl -fsSL https://agentkey.app/update-desktop.sh)",
- "update_command_kind": "shell",
+ "update_doc_url": "https://github.com/chainbase-labs/agentkey/releases/latest",
"release_notes_url": "https://github.com/chainbase-labs/agentkey/releases/tag/v1.3.0"
}
diff --git a/protocol/example-response-offline.json b/protocol/example-response-offline.json
index 7daf99f..3a3b162 100644
--- a/protocol/example-response-offline.json
+++ b/protocol/example-response-offline.json
@@ -2,5 +2,5 @@
"protocol_version": 1,
"skill_version_latest": "",
"client_detected": "claude-code",
- "update_doc_url": "https://agentkey.app/docs/upgrade"
+ "update_doc_url": "https://github.com/chainbase-labs/agentkey/releases/latest"
}
diff --git a/protocol/example-response-unknown-client.json b/protocol/example-response-unknown-client.json
index c6ec426..e9b8eb6 100644
--- a/protocol/example-response-unknown-client.json
+++ b/protocol/example-response-unknown-client.json
@@ -2,5 +2,5 @@
"protocol_version": 1,
"skill_version_latest": "1.3.0",
"client_detected": "unknown",
- "update_doc_url": "https://agentkey.app/docs/upgrade"
+ "update_doc_url": "https://github.com/chainbase-labs/agentkey/releases/latest"
}
diff --git a/protocol/skill-meta-v1.md b/protocol/skill-meta-v1.md
index 0cbf91c..b83bed0 100644
--- a/protocol/skill-meta-v1.md
+++ b/protocol/skill-meta-v1.md
@@ -115,12 +115,12 @@ The tool MUST NOT throw under any of the above; throwing would crash the agent's
| `client_detected` | `update_command_kind` | `update_command` |
|---|---|---|
| `claude-code` | `shell` | `npx -y skills update -g agentkey` |
-| `claude` | `shell` | `bash <(curl -fsSL https://agentkey.app/update-desktop.sh)` |
| `cursor` | `shell` | `npx -y skills update -g agentkey` |
| `codex` | `shell` | `npx -y skills update -g agentkey` |
-| `unknown` | (omit) | (omit) — agent falls back to `update_doc_url` |
+| `claude` (Desktop)| (omit) | (omit) — skill falls back to `update_doc_url` (GitHub releases) for manual download |
+| `unknown` | (omit) | (omit) — skill falls back to `update_doc_url` |
-The Desktop one-liner is a server-side responsibility; the script it points at is responsible for locating the Desktop sandbox path and writing the new skill files. It is NOT this repo's responsibility, but the URL MUST be live before `update_command_kind: "shell"` is emitted for `client: "claude"`.
+Desktop deliberately omits a `shell` command: Desktop installs skills into a sandboxed `~/Library/Application Support/Claude/local-agent-mode-sessions/skills-plugin//...` path which is not reachable by `npx skills update`, and no first-party scripted upgrade exists yet. Until one ships, the skill rule directs Desktop users to download the release archive from GitHub and replace the files manually. When a Desktop installer ships, this row can be promoted to `kind: "shell"` without bumping `protocol_version`.
## Skill behavior (this repo)
diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md
index 0a17911..fb70ca7 100644
--- a/skills/agentkey/SKILL.md
+++ b/skills/agentkey/SKILL.md
@@ -61,48 +61,55 @@ if [ "${AGENTKEY_AUTO_UPGRADE:-0}" = "1" ] || [ -f "${XDG_CONFIG_HOME:-$HOME/.co
```
If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\ → v\…", run **Step C**, then continue to step 0.C. **Do not** show the AskUserQuestion prompt.
-**Step B — Otherwise, prompt the user with AskUserQuestion:**
+**Step B — Otherwise, prompt the user.**
+
+If a Bash tool is available (Claude Code etc.), use `AskUserQuestion`. Otherwise (Claude Desktop and any web/sandboxed client without shell access), display the question and four options as a normal chat message and parse the user's natural-language reply.
+
+**Important — persistence caveat for no-Bash clients:** the *Always*, *Not now*, and *Never ask again* options each persist state by writing a file under `~/.config/agentkey/`. Without a Bash tool you **cannot** write those files. Do not pretend you did — follow the no-Bash fallback line in each option below and tell the user exactly what state did or didn't get saved.
+
- Question: `AgentKey v is available (currently on v). Upgrade now?`
- Options:
- **`Yes, upgrade now`** → run **Step C**.
- - **`Always keep me up to date`** → run:
- ```bash
- mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" && touch "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/auto-upgrade"
- ```
- Tell the user "Auto-upgrade enabled — future AgentKey updates install automatically. Remove `~/.config/agentkey/auto-upgrade` to undo." Then run **Step C**.
- - **`Not now`** → run:
- ```bash
- _CFG="${XDG_CONFIG_HOME:-$HOME/.config}/agentkey"
- _SNOOZE="$_CFG/update-snoozed"
- _NEW=""
- _LEVEL=0
- if [ -f "$_SNOOZE" ]; then
- _SVER=$(awk '{print $1}' "$_SNOOZE" 2>/dev/null)
- [ "$_SVER" = "$_NEW" ] && _LEVEL=$(awk '{print $2}' "$_SNOOZE" 2>/dev/null)
- case "$_LEVEL" in *[!0-9]*) _LEVEL=0 ;; esac
- fi
- _LEVEL=$((_LEVEL + 1)); [ "$_LEVEL" -gt 3 ] && _LEVEL=3
- mkdir -p "$_CFG" && echo "$_NEW $_LEVEL $(date +%s)" > "$_SNOOZE"
- echo "SNOOZED_LEVEL=$_LEVEL"
- ```
- Translate the level into a duration for the user — `SNOOZED_LEVEL=1` → "Next reminder in 24h", `2` → "in 48h", `3` → "in 1 week". Continue to step 0.C — **do not** upgrade.
- - **`Never ask again`** → run:
- ```bash
- mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" && touch "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/update-disabled"
- ```
- Tell the user "Update checks disabled. Remove `~/.config/agentkey/update-disabled` to re-enable." Continue to step 0.C — **do not** upgrade.
+ - **`Always keep me up to date`** →
+ - **With Bash:** run `mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" && touch "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/auto-upgrade"`. Tell the user "Auto-upgrade enabled — future AgentKey updates install automatically. Remove `~/.config/agentkey/auto-upgrade` to undo." Then run **Step C**.
+ - **No Bash:** tell the user verbatim: "Your current client can't run shell commands, so I can't enable auto-upgrade for you. To turn it on, run this in your terminal once: `mkdir -p ~/.config/agentkey && touch ~/.config/agentkey/auto-upgrade`. For now I'll proceed with this one-time upgrade." Then run **Step C**.
+ - **`Not now`** →
+ - **With Bash:** run the snooze script:
+ ```bash
+ _CFG="${XDG_CONFIG_HOME:-$HOME/.config}/agentkey"
+ _SNOOZE="$_CFG/update-snoozed"
+ _NEW=""
+ _LEVEL=0
+ if [ -f "$_SNOOZE" ]; then
+ _SVER=$(awk '{print $1}' "$_SNOOZE" 2>/dev/null)
+ [ "$_SVER" = "$_NEW" ] && _LEVEL=$(awk '{print $2}' "$_SNOOZE" 2>/dev/null)
+ case "$_LEVEL" in *[!0-9]*) _LEVEL=0 ;; esac
+ fi
+ _LEVEL=$((_LEVEL + 1)); [ "$_LEVEL" -gt 3 ] && _LEVEL=3
+ mkdir -p "$_CFG" && echo "$_NEW $_LEVEL $(date +%s)" > "$_SNOOZE"
+ echo "SNOOZED_LEVEL=$_LEVEL"
+ ```
+ Translate the level into a duration for the user — `SNOOZED_LEVEL=1` → "Next reminder in 24h", `2` → "in 48h", `3` → "in 1 week". Continue to step 0.C — **do not** upgrade.
+ - **No Bash:** tell the user verbatim: "Skipping for now. Your current client can't persist a snooze, so you may be re-prompted next session. To silence prompts for longer, run in a terminal once: `mkdir -p ~/.config/agentkey && touch ~/.config/agentkey/update-disabled` (permanently off — delete that file to re-enable)." Continue to step 0.C — **do not** upgrade.
+ - **`Never ask again`** →
+ - **With Bash:** run `mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" && touch "${XDG_CONFIG_HOME:-$HOME/.config}/agentkey/update-disabled"`. Tell the user "Update checks disabled. Remove `~/.config/agentkey/update-disabled` to re-enable." Continue to step 0.C — **do not** upgrade.
+ - **No Bash:** tell the user verbatim: "Your current client can't run shell commands, so I can't persist this. To disable update checks permanently, run in a terminal once: `mkdir -p ~/.config/agentkey && touch ~/.config/agentkey/update-disabled`. I'll skip this prompt for the rest of this session." Continue to step 0.C — **do not** upgrade.
**Step C — Run the upgrade.**
-If trigger was (A) and `response.update_command` is present:
-- `update_command_kind === "shell"` → Display the command verbatim and offer to run it in Bash if a Bash tool is available. If no Bash tool (e.g. Claude Desktop), instruct the user to copy-paste it into their terminal.
-- `update_command_kind === "manual_ui"` (or anything else) → Display `response.update_command` as instructions; do **not** attempt to execute. Always also offer `response.update_doc_url` as the doc link.
+Branch by trigger:
+
+**(A) Server-beacon trigger** — `response.update_command` decides:
+- `update_command_kind === "shell"` → Display the command verbatim. If a Bash tool is available, offer to run it for the user; otherwise instruct them to paste it into their terminal.
+- `update_command_kind === "manual_ui"` (or any unrecognized future kind) → Display `response.update_command` as instructions only; do **not** attempt to execute.
+- `response.update_command` is absent → No automated path exists for this client. Tell the user verbatim, substituting `` and the actual URL:
+ > AgentKey skill v\ is available but your client doesn't have an auto-installer. Download the latest release manually from GitHub: **\**. Then replace your skill files with the contents of `skills/agentkey/` from the release archive and restart your client.
-If trigger was (B), or trigger (A) had no `update_command` (only `update_doc_url`), fall back to the default Claude-Code-friendly path:
+**(B) Inline-check trigger (Claude Code with Bash)** — run:
```bash
npx skills update agentkey
```
-On success: tell the user "✓ AgentKey updated to v\." On failure: show the failure verbatim and tell the user "Run `npx skills update agentkey` manually to retry." Either way, continue to step 0.C.
+On success: tell the user "✓ AgentKey updated to v\." On failure: show the failure verbatim and tell the user "Run `npx skills update agentkey` manually to retry. If that doesn't work for your client, download from https://github.com/chainbase-labs/agentkey/releases/latest instead." Either way, continue to step 0.C.
Then route by intent:
- "setup"/"install"/"api key"/"reinstall" → **Setup**