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..85a2761
--- /dev/null
+++ b/docs/SERVER-IMPLEMENTATION.md
@@ -0,0 +1,293 @@
+# 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" },
+ "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 {
+ 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";
+}
+```
+
+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
+
+```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://github.com/chainbase-labs/agentkey/releases/latest",
+ };
+ }
+ 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://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}` } : {}),
+ };
+}
+```
+
+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://github.com/chainbase-labs/agentkey/releases/latest",
+ };
+ }
+ 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..65dd829
--- /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://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
new file mode 100644
index 0000000..4f84f00
--- /dev/null
+++ b/protocol/example-response-claude-desktop.json
@@ -0,0 +1,7 @@
+{
+ "protocol_version": 1,
+ "skill_version_latest": "1.3.0",
+ "client_detected": "claude",
+ "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
new file mode 100644
index 0000000..3a3b162
--- /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://github.com/chainbase-labs/agentkey/releases/latest"
+}
diff --git a/protocol/example-response-unknown-client.json b/protocol/example-response-unknown-client.json
new file mode 100644
index 0000000..e9b8eb6
--- /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://github.com/chainbase-labs/agentkey/releases/latest"
+}
diff --git a/protocol/skill-meta-v1.md b/protocol/skill-meta-v1.md
new file mode 100644
index 0000000..b83bed0
--- /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` |
+| `cursor` | `shell` | `npx -y skills update -g agentkey` |
+| `codex` | `shell` | `npx -y skills update -g agentkey` |
+| `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` |
+
+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)
+
+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/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
diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md
index 59455fb..fb70ca7 100644
--- a/skills/agentkey/SKILL.md
+++ b/skills/agentkey/SKILL.md
@@ -22,61 +22,94 @@ 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.
+
+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.
-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.
+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.**
+
+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.
-**Step B — Otherwise, prompt the user with AskUserQuestion:**
- 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 2 — **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.
-
-**Step C — Run the upgrade.** Invoke:
+ - **`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.**
+
+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.
+
+**(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 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. 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**