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/README.md b/README.md index 1ed0110..85f3a82 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.
@@ -225,6 +225,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 923487d..ce3c013 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 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。
@@ -225,6 +225,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` 即可。 + +
+
好像哪里不对?怎么排查? diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 7c24fcc..0dfafd6 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -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" diff --git a/skills/agentkey/SKILL.md b/skills/agentkey/SKILL.md index 4b6a7f1..58ca9e3 100644 --- a/skills/agentkey/SKILL.md +++ b/skills/agentkey/SKILL.md @@ -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 ` → 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 ` → 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 ...` → 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 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. @@ -70,9 +83,29 @@ If a Bash tool is available (Claude Code etc.), use `AskUserQuestion`. Otherwise - Question: `AgentKey v is available (currently on v). 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: "", to_version: "", 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: "", to_version: "", choice: "accept_always" + }} + }) + ``` - **`Not now`** → - **With Bash:** run the snooze script: ```bash @@ -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: "", to_version: "", 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: "", to_version: "", choice: "never_ask" + }} + }) + ``` + **Step C — Run the upgrade.** Branch by trigger: @@ -111,6 +164,25 @@ 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. 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: "", 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** diff --git a/skills/agentkey/scripts/check-update.sh b/skills/agentkey/scripts/check-update.sh index 9bfe669..246c72b 100755 --- a/skills/agentkey/scripts/check-update.sh +++ b/skills/agentkey/scripts/check-update.sh @@ -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) @@ -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 @@ -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. @@ -136,6 +201,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 @@ -143,6 +209,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 new file mode 100644 index 0000000..c7eb86d --- /dev/null +++ b/tests/check-update.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats +load helpers + +setup() { + setup_isolated_env +} + +teardown() { + teardown_isolated_env +} + +@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" ] +} + +@test "set_local_version helper correctly overrides embedded version" { + 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 文件不会 + # 让脚本崩。 +} + +@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"* ]] +} diff --git a/tests/helpers.bash b/tests/helpers.bash new file mode 100644 index 0000000..b929406 --- /dev/null +++ b/tests/helpers.bash @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# 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)" + 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" + + # 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 call mock_curl_release explicitly. + 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" </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" "$@" +}