feat: server-beacon skill-update path for non-Bash clients (#39)

## Summary

Fixes the silent-update-failure mode where Claude Desktop (and any MCP
client without a Bash tool) gets stuck on whatever skill version shipped
at first install. On this developer's Desktop the skill had been frozen
at `0.1.2` since April — no upgrade ever fired.

Root cause is structural: SKILL.md Step 0's update check uses an inline
` ```bash ``` ` block. Claude Code executes it; Desktop reads it as
documentation. So the entire upgrade flow is dead code on Desktop. This
PR routes the version check through the MCP server instead (always-on,
available to every client), and tightens a couple of correctness bugs in
the existing install/uninstall path while we're here.

Companion PR: chainbase-labs/AgentKey-Server (server-side
`agentkey_skill_meta` tool).

## What's in here

1. **Protocol** (`protocol/skill-meta-v1.md` +
`skill-meta-v1.schema.json` + 4 fixtures) — versioned,
additive-evolution wire format for an MCP meta tool that returns
`{skill_version_latest, client_detected, update_command, update_doc_url,
…}`. Spec lives in this repo (single source of truth); server vendors a
copy and CI on both sides diffs them.
2. **SKILL.md** — Step 0 now has 0.A (beacon, cross-client) → 0.B
(inline bash, Code-only compat) → 0.C (MCP tool sanity check). Step B
branches every persistence option on whether Bash is available, with
explicit no-Bash fallback text that tells the user what didn't get saved
and the exact terminal command to persist it manually. Step C points the
non-shell fallback at GitHub Releases (we don't have a docs site).
3. **install/uninstall scripts** — `npx skills remove
chainbase-labs/agentkey` was the wrong invocation: the CLI takes the
skill name (`agentkey`), exits 0 on no-match, and made the uninstaller
falsely report success. Same class of silent-success bug in `install.sh`
when `git clone` fails mid-run. Both fixed; added post-install
filesystem verification.
4. **README / README_zh** — accurate per-client update story, including
a one-time bootstrap command for users currently stuck on a pre-1.4.0
skill on Desktop.
5. **CI** (`protocol-validate.yml`) — every fixture validates against
the schema, schema rejects 4 known-bad payloads (regression guard), spec
doc references every fixture (forces docs ↔ artifact sync).
6. **`docs/SERVER-IMPLEMENTATION.md`** — handoff doc for the server PR.

## How verified

- 4/4 fixtures pass schema; 4/4 bad payloads correctly rejected
- All cross-references in spec doc resolve
- `verify-version-sync` awk still extracts `1.3.0` from SKILL.md
frontmatter
- Companion server PR exercises the actual MCP handshake (initialize +
tools/list + tools/call); response is valid v1 JSON
- Real GitHub Releases fetch + ETag caching works on the server side

## Test plan

- [ ] CI green (`protocol-validate.yml` and `verify-version-sync.yml`
both pass)
- [ ] Companion server PR merged + new `@agentkey/mcp` published
- [ ] Release-please cuts `v1.4.0` from this branch
- [ ] On Claude Code: existing inline-bash Step 0 still fires for users
on `v1.3.x`; they get prompted to update normally
- [ ] On Claude Desktop with a pre-1.4.0 skill: user runs the README
bootstrap command once to land `v1.4.0`; from that point on, every
subsequent version is auto-discovered via the meta tool
- [ ] On Cursor / Codex: meta tool returns the `npx skills update -g
agentkey` recipe; user upgrades via shell

## Notes for the reviewer

- This is **additive**: Claude Code's existing inline-bash path is
unchanged, so no regression risk there. The protocol's
`protocol_version: 1` + immortal `update_doc_url` fallback make future
v2 servers safely degradable for v1 skills.
- Claude Desktop deliberately has no `update_command` recipe yet —
Desktop installs skills into a sandboxed `~/Library/Application
Support/Claude/local-agent-mode-sessions/skills-plugin/<UUID>/...` path
that no external CLI can reach, and we don't have a first-party
installer script. The skill rule's "no command → point at GitHub
Releases" fallback handles this until one exists. Adding a Desktop
recipe later is a non-breaking change (one row in the server's `RECIPES`
map).
This commit is contained in:
不白
2026-05-12 17:46:55 +08:00
committed by GitHub
parent f05e501e42
commit 65fb2f8181
15 changed files with 835 additions and 56 deletions
+84
View File
@@ -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
+31 -4
View File
@@ -144,13 +144,38 @@ Just top up. No auto-renewal, no hidden charges.
<details>
<summary><b>How do I update?</b></summary>
**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 <UUID1>/<UUID2> 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.
</details>
+31 -4
View File
@@ -144,13 +144,38 @@ Claude 与 ChatGPT 的原生联网与平台覆盖有限,往往触达不到推
<details>
<summary><b>怎么更新?</b></summary>
**默认不用你管,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
# 把 <UUID1>/<UUID2> 替换成 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`。
</details>
+293
View File
@@ -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<string> | null = null;
export async function getLatestSkillVersion(): Promise<string> {
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<Cache | null> {
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<Cache | null> {
try {
const raw = await readFile(CACHE_PATH, "utf8");
return JSON.parse(raw);
} catch {
return null;
}
}
async function writeCache(c: Cache): Promise<void> {
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<string, Recipe> = {
"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
@@ -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"
}
@@ -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"
}
+6
View File
@@ -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"
}
@@ -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"
}
+174
View File
@@ -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/<UUID>/...` 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
+55
View File
@@ -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"]
}
}
+27
View File
@@ -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'
+21
View File
@@ -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"
+10 -5
View File
@@ -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
}
}
+10 -5
View File
@@ -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
+71 -38
View File
@@ -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 <old> <new>` → run the **Upgrade flow** below, then continue to step 2.
0.A — **Server beacon check (cross-client; runs on every client including Claude Desktop):**
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 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 <old> <new>` → 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 <old> <new>`. Substitute `<old>` and `<new>` 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 `<old>` is this SKILL.md's frontmatter version; `<new>` is `response.skill_version_latest`.
- **(B)** Step 0.B: `check-update.sh` printed `UPGRADE_AVAILABLE <old> <new>`. Use `<old>` and `<new>` from that line.
Below, `<old>` and `<new>` 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\<old\> → v\<new\>…", 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\<old\> → v\<new\>…", 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<new> is available (currently on v<old>). 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="<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.
- **`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="<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.** Invoke:
**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 `<new>` and the actual URL:
> AgentKey skill v\<new\> is available but your client doesn't have an auto-installer. Download the latest release manually from GitHub: **\<release_notes_url, if response contains one, otherwise https://github.com/chainbase-labs/agentkey/releases/latest\>**. 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\<new\>." 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\<new\>." 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**