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