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