From d96ec3d20f35cdc56ceecbf705e90be5d5a9ea2e Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 02:56:49 +0800 Subject: [PATCH 01/11] test: bootstrap bats harness for skill scripts Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/scripts-test.yml | 28 ++++++++++++++++++ tests/check-update.bats | 17 +++++++++++ tests/helpers.bash | 47 ++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 .github/workflows/scripts-test.yml create mode 100644 tests/check-update.bats create mode 100644 tests/helpers.bash diff --git a/.github/workflows/scripts-test.yml b/.github/workflows/scripts-test.yml new file mode 100644 index 0000000..1018093 --- /dev/null +++ b/.github/workflows/scripts-test.yml @@ -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/ diff --git a/tests/check-update.bats b/tests/check-update.bats new file mode 100644 index 0000000..5cf7e02 --- /dev/null +++ b/tests/check-update.bats @@ -0,0 +1,17 @@ +#!/usr/bin/env bats +load helpers + +setup() { + setup_isolated_env +} + +teardown() { + teardown_isolated_env +} + +@test "check-update.sh exits silently when version.txt missing" { + rm -f "$PLUGIN_ROOT/version.txt" + run bash "$PLUGIN_ROOT/skills/agentkey/scripts/check-update.sh" + [ "$status" -eq 0 ] + [ -z "$output" ] +} diff --git a/tests/helpers.bash b/tests/helpers.bash new file mode 100644 index 0000000..f2e4052 --- /dev/null +++ b/tests/helpers.bash @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Shared bats helpers — isolate HOME, TMPDIR, network for each test. + +setup_isolated_env() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + export TEST_TMP="$(mktemp -d)" + export HOME="$TEST_TMP/home" + export TMPDIR="$TEST_TMP/tmp" + export XDG_CONFIG_HOME="$HOME/.config" + mkdir -p "$HOME" "$TMPDIR" "$XDG_CONFIG_HOME/agentkey" + + # Use a copy of the plugin root so tests can mutate version.txt etc + export PLUGIN_ROOT="$TEST_TMP/plugin" + mkdir -p "$PLUGIN_ROOT/skills/agentkey/scripts" + cp "$REPO_ROOT/skills/agentkey/scripts/check-update.sh" \ + "$PLUGIN_ROOT/skills/agentkey/scripts/check-update.sh" + # version.txt may not exist yet (version is currently embedded in check-update.sh); + # later tasks will introduce it. Copy if present, otherwise skip silently. + if [ -f "$REPO_ROOT/version.txt" ]; then + cp "$REPO_ROOT/version.txt" "$PLUGIN_ROOT/version.txt" + fi + export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" + + # Block real network — every test must mock curl + mkdir -p "$TEST_TMP/bin" + cat > "$TEST_TMP/bin/curl" <<'EOF' +#!/usr/bin/env bash +echo "ERROR: curl not mocked in this test" >&2 +exit 7 +EOF + chmod +x "$TEST_TMP/bin/curl" + export PATH="$TEST_TMP/bin:$PATH" +} + +teardown_isolated_env() { + [ -n "$TEST_TMP" ] && rm -rf "$TEST_TMP" +} + +# Mock curl to return a fixed GitHub /releases/latest payload. +mock_curl_release() { + local tag="$1" + cat > "$TEST_TMP/bin/curl" < Date: Tue, 12 May 2026 03:04:19 +0800 Subject: [PATCH 02/11] test: revise harness for embedded-version check-update.sh --- tests/check-update.bats | 11 ++++++++--- tests/helpers.bash | 40 +++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/tests/check-update.bats b/tests/check-update.bats index 5cf7e02..ef3b308 100644 --- a/tests/check-update.bats +++ b/tests/check-update.bats @@ -9,9 +9,14 @@ teardown() { teardown_isolated_env } -@test "check-update.sh exits silently when version.txt missing" { - rm -f "$PLUGIN_ROOT/version.txt" - run bash "$PLUGIN_ROOT/skills/agentkey/scripts/check-update.sh" +@test "exits silently when update-disabled file exists" { + touch "$XDG_CONFIG_HOME/agentkey/update-disabled" + run_check_update [ "$status" -eq 0 ] [ -z "$output" ] } + +@test "set_local_version helper correctly overrides embedded version" { + set_local_version "9.9.9" + grep -q '^LOCAL_VERSION="9.9.9"' "$SCRIPT" +} diff --git a/tests/helpers.bash b/tests/helpers.bash index f2e4052..b929406 100644 --- a/tests/helpers.bash +++ b/tests/helpers.bash @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# Shared bats helpers — isolate HOME, TMPDIR, network for each test. +# Shared bats helpers — isolate HOME, TMPDIR, network per test. +# main 上的 check-update.sh 把版本内嵌在脚本里 +# (`LOCAL_VERSION="x.y.z" # x-release-please-version`),本 helper 提供 +# `set_local_version` 直接覆盖那一行以模拟不同的本地版本。 setup_isolated_env() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" @@ -9,19 +12,13 @@ setup_isolated_env() { export XDG_CONFIG_HOME="$HOME/.config" mkdir -p "$HOME" "$TMPDIR" "$XDG_CONFIG_HOME/agentkey" - # Use a copy of the plugin root so tests can mutate version.txt etc - export PLUGIN_ROOT="$TEST_TMP/plugin" - mkdir -p "$PLUGIN_ROOT/skills/agentkey/scripts" - cp "$REPO_ROOT/skills/agentkey/scripts/check-update.sh" \ - "$PLUGIN_ROOT/skills/agentkey/scripts/check-update.sh" - # version.txt may not exist yet (version is currently embedded in check-update.sh); - # later tasks will introduce it. Copy if present, otherwise skip silently. - if [ -f "$REPO_ROOT/version.txt" ]; then - cp "$REPO_ROOT/version.txt" "$PLUGIN_ROOT/version.txt" - fi - export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" + # Copy the script into the test sandbox so we can mutate LOCAL_VERSION. + export SCRIPT_DIR="$TEST_TMP/scripts" + export SCRIPT="$SCRIPT_DIR/check-update.sh" + mkdir -p "$SCRIPT_DIR" + cp "$REPO_ROOT/skills/agentkey/scripts/check-update.sh" "$SCRIPT" - # Block real network — every test must mock curl + # Block real network — every test must call mock_curl_release explicitly. mkdir -p "$TEST_TMP/bin" cat > "$TEST_TMP/bin/curl" <<'EOF' #!/usr/bin/env bash @@ -45,3 +42,20 @@ echo '{"tag_name":"$tag"}' EOF chmod +x "$TEST_TMP/bin/curl" } + +# Override the embedded LOCAL_VERSION in the sandboxed copy of check-update.sh. +set_local_version() { + local v="$1" + # GNU sed and BSD sed both accept this in-place form on Linux/macOS via the + # trailing empty string trick: use a portable sed wrapper. + if sed --version >/dev/null 2>&1; then + sed -i "s/^LOCAL_VERSION=.*/LOCAL_VERSION=\"$v\" # x-release-please-version/" "$SCRIPT" + else + sed -i '' "s/^LOCAL_VERSION=.*/LOCAL_VERSION=\"$v\" # x-release-please-version/" "$SCRIPT" + fi +} + +# Run the sandboxed check-update.sh and capture status + output. +run_check_update() { + run bash "$SCRIPT" "$@" +} From 953b7f6e670c7bdcf2f088dbf3b27eb3cb286eb5 Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:05:38 +0800 Subject: [PATCH 03/11] feat(check-update): add telemetry opt-out plumbing --- skills/agentkey/scripts/check-update.sh | 10 ++++++++++ tests/check-update.bats | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index c447faa..c3e9d46 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -38,6 +38,16 @@ 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 diff --git a/tests/check-update.bats b/tests/check-update.bats index ef3b308..4d307c0 100644 --- a/tests/check-update.bats +++ b/tests/check-update.bats @@ -20,3 +20,15 @@ teardown() { set_local_version "9.9.9" grep -q '^LOCAL_VERSION="9.9.9"' "$SCRIPT" } + +@test "telemetry-disabled file does not break existing update flow" { + touch "$XDG_CONFIG_HOME/agentkey/telemetry-disabled" + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + run_check_update + [ "$status" -eq 0 ] + # 行为不变:UP_TO_DATE 仍然输出 + [[ "$output" == *"UP_TO_DATE"* ]] + # Task 2 还没引入 emit,Task 3 才会加;此处主要保 telemetry-disabled 文件不会 + # 让脚本崩。 +} From 8840eb8d4bf5cf84144c9879b6ef2621b9451ee2 Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:07:34 +0800 Subject: [PATCH 04/11] feat(check-update): emit TELEMETRY skill_loaded lines with 24h dedup --- skills/agentkey/scripts/check-update.sh | 40 +++++++++++++ tests/check-update.bats | 74 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index c3e9d46..53949bf 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -49,8 +49,42 @@ emit_telemetry_enabled() { return 0 } +# 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 + mtime=$(stat -f %m "$hb" 2>/dev/null || stat -c %Y "$hb" 2>/dev/null || echo 0) + 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' +} + # 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 @@ -113,14 +147,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. @@ -146,6 +183,7 @@ 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 @@ -153,6 +191,8 @@ fi 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)" diff --git a/tests/check-update.bats b/tests/check-update.bats index 4d307c0..c7eb86d 100644 --- a/tests/check-update.bats +++ b/tests/check-update.bats @@ -11,6 +11,9 @@ teardown() { @test "exits silently when update-disabled file exists" { touch "$XDG_CONFIG_HOME/agentkey/update-disabled" + # Disable telemetry so the silent-exit contract is unaffected by Task 3's + # emit on the update-disabled branch. + touch "$XDG_CONFIG_HOME/agentkey/telemetry-disabled" run_check_update [ "$status" -eq 0 ] [ -z "$output" ] @@ -32,3 +35,74 @@ teardown() { # Task 2 还没引入 emit,Task 3 才会加;此处主要保 telemetry-disabled 文件不会 # 让脚本崩。 } + +@test "emits TELEMETRY skill_loaded up_to_date when versions match" { + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + run_check_update + [ "$status" -eq 0 ] + [[ "$output" == *"UP_TO_DATE"* ]] + [[ "$output" == *"TELEMETRY skill_loaded"* ]] + [[ "$output" == *"update_state=up_to_date"* ]] + [[ "$output" == *"skill_version=1.0.0"* ]] +} + +@test "emits TELEMETRY skill_loaded upgrade_available when newer release exists" { + set_local_version "1.0.0" + mock_curl_release "v2.0.0" + run_check_update + [ "$status" -eq 0 ] + [[ "$output" == *"UPGRADE_AVAILABLE 1.0.0 2.0.0"* ]] + [[ "$output" == *"TELEMETRY skill_loaded"* ]] + [[ "$output" == *"update_state=upgrade_available"* ]] + [[ "$output" == *"latest_version=2.0.0"* ]] +} + +@test "emits TELEMETRY skill_loaded disabled when update-disabled file exists" { + touch "$XDG_CONFIG_HOME/agentkey/update-disabled" + set_local_version "1.0.0" + # 注意 update-disabled 早返发生在 curl 之前,所以不需要 mock_curl_release。 + run_check_update + [ "$status" -eq 0 ] + [[ "$output" == *"TELEMETRY skill_loaded"* ]] + [[ "$output" == *"update_state=disabled"* ]] + # update-disabled 不应再输出 UP_TO_DATE / UPGRADE_AVAILABLE 主行 + [[ "$output" != *"UP_TO_DATE"* ]] + [[ "$output" != *"UPGRADE_AVAILABLE"* ]] +} + +@test "second invocation within 24h does not re-emit telemetry" { + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + run_check_update + [[ "$output" == *"TELEMETRY skill_loaded"* ]] + + run_check_update + [[ "$output" == *"UP_TO_DATE"* ]] + [[ "$output" != *"TELEMETRY"* ]] +} + +@test "AGENTKEY_TELEMETRY=0 disables emit" { + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + AGENTKEY_TELEMETRY=0 run_check_update + [[ "$output" == *"UP_TO_DATE"* ]] + [[ "$output" != *"TELEMETRY"* ]] +} + +@test "telemetry-disabled file disables emit" { + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + touch "$XDG_CONFIG_HOME/agentkey/telemetry-disabled" + run_check_update + [[ "$output" == *"UP_TO_DATE"* ]] + [[ "$output" != *"TELEMETRY"* ]] +} + +@test "emits auto_upgrade_enabled=1 when auto-upgrade file exists" { + set_local_version "1.0.0" + mock_curl_release "v1.0.0" + touch "$XDG_CONFIG_HOME/agentkey/auto-upgrade" + run_check_update + [[ "$output" == *"auto_upgrade_enabled=1"* ]] +} From cfbb3286f668385fbfdaf1d1d9ecc1c69f9457bc Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:08:18 +0800 Subject: [PATCH 05/11] feat(skill): parse TELEMETRY lines and dispatch via MCP --- skills/agentkey/SKILL.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md index fcc72df..760b0f0 100644 --- a/skills/agentkey/SKILL.md +++ b/skills/agentkey/SKILL.md @@ -18,8 +18,22 @@ license: MIT ```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. + The script may output **two kinds of lines** (in any order): + + - **Update line** (one of): + - `UP_TO_DATE` or empty → continue silently to step 2. + - `UPGRADE_AVAILABLE ` → run the **Upgrade flow** below, then continue to step 2. + + - **Telemetry line** (zero or one): + - `TELEMETRY ...` → forward via MCP, silently and best-effort: + ``` + execute_tool(name="agentkey_internal", params={ + path: "telemetry/event", + params: { 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 step 2. 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. From bbbf56cabbee84b43993dacaaacea2a0bbb93932 Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:09:19 +0800 Subject: [PATCH 06/11] feat(skill): dispatch upgrade_decision and upgrade_result telemetry --- skills/agentkey/SKILL.md | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md index 760b0f0..6a410d9 100644 --- a/skills/agentkey/SKILL.md +++ b/skills/agentkey/SKILL.md @@ -51,11 +51,29 @@ If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\ - Question: `AgentKey v is available (currently on v). Upgrade now?` - Options: - **`Yes, upgrade now`** → 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: "", to_version: "", choice: "accept_once" + }} + }) + ``` - **`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**. + After the action, dispatch (silently, ignore failures): + ``` + execute_tool(name="agentkey_internal", params={ + path: "telemetry/event", + params: { event: "upgrade_decision", props: { + from_version: "", to_version: "", choice: "accept_always" + }} + }) + ``` - **`Not now`** → run: ```bash _CFG="${XDG_CONFIG_HOME:-$HOME/.config}/agentkey" @@ -72,11 +90,29 @@ If the output is `AUTO=1`: tell the user once "Auto-upgrading AgentKey v\ 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. + Map `SNOOZED_LEVEL` to choice: `1` → `snooze_1d`, `2` → `snooze_2d`, `3` → `snooze_7d`. Then dispatch (silently, ignore failures): + ``` + execute_tool(name="agentkey_internal", params={ + path: "telemetry/event", + params: { event: "upgrade_decision", props: { + from_version: "", to_version: "", choice: "" + }} + }) + ``` - **`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. + After the action, dispatch (silently, ignore failures): + ``` + execute_tool(name="agentkey_internal", params={ + path: "telemetry/event", + params: { event: "upgrade_decision", props: { + from_version: "", to_version: "", choice: "never_ask" + }} + }) + ``` **Step C — Run the upgrade.** Invoke: ```bash @@ -84,6 +120,25 @@ npx skills update chainbase-labs/agentkey ``` On success: tell the user "✓ AgentKey updated to v\." On failure: show the failure verbatim and tell the user "Run `npx skills update chainbase-labs/agentkey` manually to retry." Either way, continue to step 2. +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: "", to_version: "", + status: <"ok" if npx succeeded else "fail">, + error_class: + }} +}) +``` + +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** From ff666f10c09fc24e9aa7112ca8bd8d659b3bf6f3 Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:12:51 +0800 Subject: [PATCH 07/11] docs: document telemetry collection and opt-out --- README.md | 22 +++++++++++++++++++++- docs/README_zh.md | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7796f30..6c5ae81 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ AgentKey maintains cloud-side integrations with each platform — no extra accou
Is it safe? -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.
@@ -198,6 +198,26 @@ The one-command uninstaller additionally cleans npm/npx caches, legacy shell rc +
+How do I opt out of telemetry? + +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 + +# At install time +curl -fsSL https://agentkey.app/install.sh | bash -s -- --no-telemetry +``` + +To re-enable, delete `~/.config/agentkey/telemetry-disabled`. + +
+
Something's not working — how do I check? diff --git a/docs/README_zh.md b/docs/README_zh.md index 3615d3e..b869d5d 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -123,7 +123,7 @@ AgentKey 在云端维护与各平台的对接 —— 你不需要额外开账号
安全吗? -安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们就看不到你的本地文件、凭证或 Agent 的对话,也没条件采集。 +安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。
@@ -198,6 +198,26 @@ npx skills remove chainbase-labs/agentkey
+
+我如何关闭遥测? + +AgentKey 会上报匿名使用统计(你用的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭: + +```bash +# 持久关闭(推荐) +touch ~/.config/agentkey/telemetry-disabled + +# 进程级临时关闭(CI / 单次会话) +AGENTKEY_TELEMETRY=0 + +# 安装时直接关 +curl -fsSL https://agentkey.app/install.sh | bash -s -- --no-telemetry +``` + +想重新开启,删掉 `~/.config/agentkey/telemetry-disabled` 即可。 + +
+
好像哪里不对?怎么排查? From ddad4c3ce6e3cd6e572cc665da23a219bb6c1ea5 Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:13:26 +0800 Subject: [PATCH 08/11] fix(uninstall): clean ~/.config/agentkey/ telemetry & upgrade state --- scripts/uninstall.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 606f36c..ecefc28 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -314,6 +314,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" From f7f266fcf1856c3000409b2b9b9e7d7d69c0dcde Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 03:15:51 +0800 Subject: [PATCH 09/11] fix(check-update): swap stat -c / -f order for Linux compat On Linux, `stat -f %m` returns the filesystem mountpoint (a string), not the file mtime. The previous order (`-f` first, `-c` second) meant the `||` fallback never fired on Linux and the heartbeat dedup math was based on garbage. Try GNU `-c %Y` first, then BSD `-f %m`. The same pattern exists in the pre-existing cache-age branch at line ~130 and likely has the same bug; left for a separate fix to keep this PR scoped to the telemetry feature. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/agentkey/scripts/check-update.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index 53949bf..4610e9c 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -69,7 +69,10 @@ emit_telemetry() { local hb="${TMPDIR:-/tmp}/agentkey-heartbeat-$LOCAL_VERSION" if [ -f "$hb" ]; then local mtime age - mtime=$(stat -f %m "$hb" 2>/dev/null || stat -c %Y "$hb" 2>/dev/null || echo 0) + # Linux GNU stat uses `-c %Y`; macOS BSD stat uses `-f %m`. Try GNU + # first because on Linux `stat -f %m` is "filesystem mountpoint", not + # mtime — it succeeds with garbage and prevents the `||` fallback. + mtime=$(stat -c %Y "$hb" 2>/dev/null || stat -f %m "$hb" 2>/dev/null || echo 0) age=$(( ${NOW:-$(date +%s)} - mtime )) if [ "$age" -ge 0 ] && [ "$age" -lt "$TELEMETRY_HEARTBEAT_TTL" ]; then return 0 From 0d900646e9210baab0bb863a780492d2e6a30b5e Mon Sep 17 00:00:00 2001 From: lxcong Date: Tue, 12 May 2026 04:00:42 +0800 Subject: [PATCH 10/11] fix(check-update): guard MTIME against polluted stat output on Ubuntu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on ubuntu-latest blew up in test 7 with "line 136: File: unbound variable" because GNU stat on Ubuntu 24.04 prints filesystem info to stdout (not stderr) when `stat -f %m FILE` is invoked invalidly: $ stat -f %m /tmp/cache 2>/dev/null File: "/tmp/cache" ID: ... Namelen: 255 Type: overlayfs Block size: 4096 ... That output flowed into MTIME=$(stat -f %m ... || stat -c %Y ...), and $((NOW - MTIME)) then tried to evaluate `NOW - File:` which under set -u threw an unbound-variable error on the word "File". Two-pronged fix in both the cache fast-path and emit_telemetry's heartbeat dedup: 1. Swap order — try GNU `-c %Y` first (Linux); `-f %m` only as macOS fallback. Avoids the polluted-stdout path entirely on Linux. 2. Numeric guard — `case "$MTIME" in ''|*[!0-9]*) MTIME=0 ;; esac` strips any non-numeric leftovers if both forms ever produce garbage (belt-and-suspenders). MTIME=0 makes AGE huge → fast-path falls through to slow-path, which is the safe behavior. Verified locally and inside ubuntu:24.04 docker: bats 10/10 passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/agentkey/scripts/check-update.sh | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index 4610e9c..e662af4 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -69,10 +69,15 @@ emit_telemetry() { 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`. Try GNU - # first because on Linux `stat -f %m` is "filesystem mountpoint", not - # mtime — it succeeds with garbage and prevents the `||` fallback. + # 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 @@ -130,9 +135,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 From fda300b5ee80d1fb346f274bc867a7603dad8517 Mon Sep 17 00:00:00 2001 From: lxcong <83766787@qq.com> Date: Thu, 14 May 2026 16:39:48 +0800 Subject: [PATCH 11/11] fix(check-update): sanity-check LOCAL_VERSION before any emit Previously the update-disabled branch could call emit_telemetry with a malformed LOCAL_VERSION (if release-please ever fails to sync the embedded version line), creating an oddly-named heartbeat file at $TMPDIR/agentkey-heartbeat-. The semver guard now runs first, so a malformed version exits silently before any filesystem write or telemetry emit. Surfaced by Claude security review on 6a1a505. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/agentkey/scripts/check-update.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index 1c42453..246c72b 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -90,19 +90,21 @@ emit_telemetry() { printf '\n' } +# 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 -# Sanity check the embedded version — if release-please ever fails to sync -# this line, exit silently rather than emit garbage. -case "$LOCAL_VERSION" in - [0-9]*.[0-9]*.[0-9]*) ;; - *) exit 0 ;; -esac - # Cache `date +%s` once — used by both the cache age math and snooze expiry. NOW=$(date +%s)