Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/scripts-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Scripts tests
on:
push:
paths:
- 'skills/agentkey/scripts/**'
- 'tests/**'
- '.github/workflows/scripts-test.yml'
pull_request:
paths:
- 'skills/agentkey/scripts/**'
- 'tests/**'

jobs:
bats:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install bats
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
brew install bats-core
else
sudo apt-get update && sudo apt-get install -y bats
fi
- run: bats tests/
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ AgentKey maintains cloud-side integrations with each platform — no extra accou
<details>
<summary><b>Is it safe?</b></summary>

Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. There's nothing for us to collect.
Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. The only data AgentKey collects is anonymous usage telemetry — which agent you installed into, your skill version, and upgrade outcomes — never your queries or responses. See "How do I opt out of telemetry?" below.

</details>

Expand Down Expand Up @@ -225,6 +225,26 @@ The one-command uninstaller additionally cleans npm/npx caches, legacy shell rc

</details>

<details>
<summary><b>How do I opt out of telemetry?</b></summary>

AgentKey sends anonymous usage telemetry (which agent you use, skill version, upgrade outcomes — never queries or responses). Three ways to opt out, any of them works:

```bash
# Persistent opt-out (recommended)
touch ~/.config/agentkey/telemetry-disabled

# One-shot env override (CI / single session)
AGENTKEY_TELEMETRY=0 <your command>

# At install time
curl -fsSL https://agentkey.app/install.sh | bash -s -- --no-telemetry
```

To re-enable, delete `~/.config/agentkey/telemetry-disabled`.

</details>

<details>
<summary><b>Something's not working — how do I check?</b></summary>

Expand Down
22 changes: 21 additions & 1 deletion docs/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ AgentKey 在云端维护与各平台的对接 —— 你不需要额外开账号
<details>
<summary><b>安全吗?</b></summary>

安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们就看不到你的本地文件、凭证或 Agent 的对话,也没条件采集
安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"

</details>

Expand Down Expand Up @@ -225,6 +225,26 @@ npx skills remove chainbase-labs/agentkey

</details>

<details>
<summary><b>我如何关闭遥测?</b></summary>

AgentKey 会上报匿名使用统计(你用的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭:

```bash
# 持久关闭(推荐)
touch ~/.config/agentkey/telemetry-disabled

# 进程级临时关闭(CI / 单次会话)
AGENTKEY_TELEMETRY=0 <your command>

# 安装时直接关
curl -fsSL https://agentkey.app/install.sh | bash -s -- --no-telemetry
```

想重新开启,删掉 `~/.config/agentkey/telemetry-disabled` 即可。

</details>

<details>
<summary><b>好像哪里不对?怎么排查?</b></summary>

Expand Down
10 changes: 10 additions & 0 deletions scripts/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ else
skipped "No ~/.npm/_npx directory"
fi

# ── 7b. AgentKey config dir (snooze/disable/telemetry state) ─────────────
step "7b. AgentKey config directory"

AGENTKEY_CFG="$HOME/.config/agentkey"
if [ -d "$AGENTKEY_CFG" ]; then
rm -rf "$AGENTKEY_CFG" && ok "Removed $AGENTKEY_CFG"
else
skipped "No $AGENTKEY_CFG directory"
fi

# ── 8. Residual artifacts ─────────────────────────────────────────────────
step "8. Residual artifacts"

Expand Down
76 changes: 74 additions & 2 deletions skills/agentkey/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,21 @@ The response is structured JSON conforming to [protocol/skill-meta-v1](https://g
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.
The script may print **two kinds of lines** (in any order):

- **Update line** (one of):
- `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.
- **Telemetry line** (zero or one):
- `TELEMETRY <event> <key=value> ...` → forward via MCP, silently and best-effort:
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "<event>", props: { /* parsed key=value pairs */ } }
})
```
- If the tool does not exist or fails, **ignore completely** — never surface a telemetry error to the user.
- Do not let the telemetry dispatch block 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.

Expand Down Expand Up @@ -70,9 +83,29 @@ If a Bash tool is available (Claude Code etc.), use `AskUserQuestion`. Otherwise
- Question: `AgentKey v<new> is available (currently on v<old>). Upgrade now?`
- Options:
- **`Yes, upgrade now`** → run **Step C**.

After running **Step C**, dispatch (silently, ignore failures):
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "upgrade_decision", props: {
from_version: "<old>", to_version: "<new>", choice: "accept_once"
}}
})
```
- **`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**.

After the action, dispatch (silently, ignore failures):
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "upgrade_decision", props: {
from_version: "<old>", to_version: "<new>", choice: "accept_always"
}}
})
```
- **`Not now`** →
- **With Bash:** run the snooze script:
```bash
Expand All @@ -91,10 +124,30 @@ If a Bash tool is available (Claude Code etc.), use `AskUserQuestion`. Otherwise
```
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.

Map the choice for telemetry: With-Bash uses `SNOOZED_LEVEL` (`1` → `snooze_1d`, `2` → `snooze_2d`, `3` → `snooze_7d`); No-Bash uses `snooze_1d` (no persisted level). Then dispatch (silently, ignore failures):
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "upgrade_decision", props: {
from_version: "<old>", to_version: "<new>", choice: "<mapped choice>"
}}
})
```
- **`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.

After the action, dispatch (silently, ignore failures):
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "upgrade_decision", props: {
from_version: "<old>", to_version: "<new>", choice: "never_ask"
}}
})
```

**Step C — Run the upgrade.**

Branch by trigger:
Expand All @@ -111,6 +164,25 @@ 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. 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.

After the `npx` command returns, dispatch (silently, ignore failures):
```
execute_tool(name="agentkey_internal", params={
path: "telemetry/event",
params: { event: "upgrade_result", props: {
from_version: "<old>", to_version: "<new>",
status: <"ok" if npx succeeded else "fail">,
error_class: <one of "network" | "npx_failed" | "permission" | "unknown" if status=="fail" else null>
}}
})
```

Decision rules for `error_class`:
- npx exit code 0 → `status: "ok"`, `error_class: null`
- npx output contains `ENOTFOUND` / `ETIMEDOUT` / `ECONNREFUSED` → `network`
- npx output contains `EACCES` / `permission denied` → `permission`
- npx ran but reported its own failure → `npx_failed`
- otherwise → `unknown`

Then route by intent:
- "setup"/"install"/"api key"/"reinstall" → **Setup**
- "status"/"diagnose" → **Status**
Expand Down
84 changes: 76 additions & 8 deletions skills/agentkey/scripts/check-update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,73 @@ CACHE_FILE="${TMPDIR:-/tmp}/agentkey-update-check"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/agentkey"
DISABLED_FILE="$CONFIG_DIR/update-disabled"
SNOOZE_FILE="$CONFIG_DIR/update-snoozed"
TELEMETRY_DISABLED_FILE="$CONFIG_DIR/telemetry-disabled"
TELEMETRY_HEARTBEAT_TTL=86400 # 24h client-side dedup

# Telemetry: the skill itself never sends — it only emits a "TELEMETRY ..."
# line to stdout for SKILL.md to dispatch via MCP. Opt-out via file or env.
emit_telemetry_enabled() {
[ "${AGENTKEY_TELEMETRY:-1}" = "0" ] && return 1
[ -f "$TELEMETRY_DISABLED_FILE" ] && return 1
return 0
}

# Disabled by user ("Never ask again") — exit silently.
if [ -f "$DISABLED_FILE" ]; then
exit 0
fi
# Inline `auto_upgrade_enabled=` kv pair for emit_telemetry callers.
auto_upgrade_flag() {
if [ "${AGENTKEY_AUTO_UPGRADE:-0}" = "1" ] || [ -f "$CONFIG_DIR/auto-upgrade" ]; then
echo "auto_upgrade_enabled=1"
else
echo "auto_upgrade_enabled=0"
fi
}

# Emit a single-line TELEMETRY event to stdout for SKILL.md to forward via MCP.
# Args: event_name kv_pairs...
# Honors opt-out (file / env) and 24h client-side dedup per LOCAL_VERSION.
# Server does the strict per-user dedup; this is just defensive bandwidth control.
emit_telemetry() {
emit_telemetry_enabled || return 0
local event="$1"; shift

local hb="${TMPDIR:-/tmp}/agentkey-heartbeat-$LOCAL_VERSION"
if [ -f "$hb" ]; then
local mtime age
# Linux GNU stat uses `-c %Y`; macOS BSD stat uses `-f %m`. GNU first
# because on Linux `-f %m` is invalid and some builds (Ubuntu 24.04 CI)
# pollute stdout with filesystem info even on failure — which would
# poison the arithmetic below under `set -u`. Numeric guard is the
# belt-and-suspenders defense.
mtime=$(stat -c %Y "$hb" 2>/dev/null || stat -f %m "$hb" 2>/dev/null || echo 0)
case "$mtime" in
''|*[!0-9]*) mtime=0 ;;
esac
age=$(( ${NOW:-$(date +%s)} - mtime ))
if [ "$age" -ge 0 ] && [ "$age" -lt "$TELEMETRY_HEARTBEAT_TTL" ]; then
return 0
fi
fi
touch "$hb" 2>/dev/null || true

printf 'TELEMETRY %s skill_version=%s' "$event" "$LOCAL_VERSION"
for kv in "$@"; do printf ' %s' "$kv"; done
printf '\n'
}

# Sanity check the embedded version — if release-please ever fails to sync
# this line, exit silently rather than emit garbage.
# Sanity check the embedded version first — if release-please ever fails to
# sync this line, exit silently rather than emit garbage. Runs before any
# emit_telemetry call so a malformed LOCAL_VERSION can't poison the heartbeat
# file path ($TMPDIR/agentkey-heartbeat-$LOCAL_VERSION).
case "$LOCAL_VERSION" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) exit 0 ;;
esac

# Disabled by user ("Never ask again") — exit silently.
if [ -f "$DISABLED_FILE" ]; then
emit_telemetry skill_loaded update_state=disabled "$(auto_upgrade_flag)"
exit 0
fi

# Cache `date +%s` once — used by both the cache age math and snooze expiry.
NOW=$(date +%s)

Expand Down Expand Up @@ -83,9 +137,17 @@ check_snooze() {

# Fast path: recent cache hit — avoids the GitHub API round-trip (~1.5s).
if [ -f "$CACHE_FILE" ]; then
MTIME=$(stat -f %m "$CACHE_FILE" 2>/dev/null \
|| stat -c %Y "$CACHE_FILE" 2>/dev/null \
# GNU `stat -c %Y` first (Linux). BSD `stat -f %m` only as fallback for
# macOS. Some GNU stat builds (Ubuntu 24.04 in CI) print filesystem info
# to stdout even when `-f %m` is invalid, which would poison MTIME and
# blow up the arithmetic below under `set -u`. The numeric guard at the
# end strips that out defensively if both forms ever produce garbage.
MTIME=$(stat -c %Y "$CACHE_FILE" 2>/dev/null \
|| stat -f %m "$CACHE_FILE" 2>/dev/null \
|| echo 0)
case "$MTIME" in
''|*[!0-9]*) MTIME=0 ;;
esac
AGE=$(( NOW - MTIME ))

# Single-pass read of the cache line. Empty / corrupted cache → all
Expand All @@ -103,14 +165,17 @@ if [ -f "$CACHE_FILE" ]; then
case "$CACHED_KIND" in
"UP_TO_DATE")
echo "UP_TO_DATE"
emit_telemetry skill_loaded update_state=up_to_date "$(auto_upgrade_flag)"
exit 0
;;
"UPGRADE_AVAILABLE")
if [ "$CACHED_OLD" = "$LOCAL_VERSION" ] && [ -n "$CACHED_NEW" ]; then
if check_snooze "$CACHED_NEW"; then
emit_telemetry skill_loaded update_state=snoozed "latest_version=$CACHED_NEW" "$(auto_upgrade_flag)"
exit 0
fi
echo "UPGRADE_AVAILABLE $CACHED_OLD $CACHED_NEW"
emit_telemetry skill_loaded update_state=upgrade_available "latest_version=$CACHED_NEW" "$(auto_upgrade_flag)"
exit 0
fi
# Local moved on — fall through to re-check.
Expand All @@ -136,13 +201,16 @@ esac
if [ "$LOCAL_VERSION" = "$LATEST_VERSION" ]; then
echo "UP_TO_DATE" > "$CACHE_FILE" 2>/dev/null || true
echo "UP_TO_DATE"
emit_telemetry skill_loaded update_state=up_to_date "$(auto_upgrade_flag)"
exit 0
fi

# Newer version available — cache the result, then suppress output if snoozed.
MSG="UPGRADE_AVAILABLE $LOCAL_VERSION $LATEST_VERSION"
echo "$MSG" > "$CACHE_FILE" 2>/dev/null || true
if check_snooze "$LATEST_VERSION"; then
emit_telemetry skill_loaded update_state=snoozed "latest_version=$LATEST_VERSION" "$(auto_upgrade_flag)"
exit 0
fi
echo "$MSG"
emit_telemetry skill_loaded update_state=upgrade_available "latest_version=$LATEST_VERSION" "$(auto_upgrade_flag)"
Loading
Loading