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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ jobs:

# Every skill must be discoverable by `npx skills add`, its frontmatter name
# must match its directory, and nothing in the repo may leak a real key or an
# internal host. The install script must at least parse.
# internal host. The install script must at least parse, every shell script
# must pass shellcheck, and the skill scripts' own tests must pass.
validate:
name: Validate skills
runs-on: ubuntu-latest
Expand Down Expand Up @@ -88,3 +89,7 @@ jobs:
fi
- name: install.sh parses
run: sh -n install.sh
- name: shellcheck scripts
run: shellcheck install.sh skills/*/scripts/*.sh
- name: blocks-api freshness check tests
run: bash skills/blocks-api/scripts/skill-freshness-check.test.sh
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ npx skills add https://github.com/BlocksOrg/skills --skill blocks-api --global

Omit `--global` to install into the current project instead of your home directory.

Once installed, the skill refreshes itself from this repo at most once a day the first time it is used. Set `BLOCKS_SKILLS_NO_UPDATE=1` to disable that.

One-liner alternative:

```bash
Expand Down
12 changes: 11 additions & 1 deletion skills/blocks-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ description: >
Blocks agent did in a session. Requires BLOCKS_API_KEY.
license: MIT
metadata:
version: "0.1.0"
version: "0.2.0"
---

## Step 0: freshness check

Before anything else, run the skill's own update check once, resolving the path relative to this file's directory:

```bash
bash scripts/skill-freshness-check.sh
```

It refreshes this skill from its source at most once a day, never prompts, and always exits 0. If it prints a note, relay it to the user (the refreshed instructions take effect next session). If it prints nothing, say nothing. Continue with the task regardless of the outcome. Set `BLOCKS_SKILLS_NO_UPDATE=1` to disable it.

## Setup

```bash
Expand Down
257 changes: 257 additions & 0 deletions skills/blocks-api/scripts/skill-freshness-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#!/usr/bin/env bash
# Keep the installed blocks-api skill fresh without getting in the agent's way.
#
# The agent runs this once per invocation of the skill. The script decides
# whether an update is due, runs `npx skills update` from the right directory,
# and reports a single line on stdout only when the skill files actually
# changed. It never fails: every path exits 0, so a broken network or a
# missing npx can never block the task the agent was asked to do.
#
# Throttling (per scope root, tracked in a small marker file):
# - after a successful check, stay quiet for 24 hours
# - after a failed check, retry 15 minutes later
# - after 3 consecutive failures, back off to one attempt per day
# - concurrent runs share a lock; only the first one checks, the rest skip
#
# Environment:
# BLOCKS_SKILLS_NO_UPDATE=1 opt out entirely (exit 0 immediately)
# BLOCKS_SKILLS_DEBUG=1 log every decision to stderr
# BLOCKS_SKILLS_NOW override the clock (epoch seconds), for tests
# BLOCKS_SKILLS_UPDATE_CMD override the update command, for tests
# BLOCKS_SKILLS_TIMEOUT override the update deadline (seconds), for tests
# BLOCKS_SKILLS_ROOT override the detected scope root, for tests
# XDG_CACHE_HOME marker files live under $XDG_CACHE_HOME/blocks-skills
# (defaults to ~/.cache)

SKILL_NAME="blocks-api"
SKILLS_CLI_VERSION="1.7.0"

SUCCESS_INTERVAL=86400 # 24h: do not re-check after a success
RETRY_INTERVAL=900 # 15m: retry after a failure
MAX_FAILURES=3 # after this many failures in a row, retry daily
UPDATE_TIMEOUT=120 # seconds before a stalled update is killed

log() {
if [ -n "${BLOCKS_SKILLS_DEBUG:-}" ]; then
printf 'skill-freshness-check: %s\n' "$*" >&2
fi
}

if [ -n "${BLOCKS_SKILLS_NO_UPDATE:-}" ]; then
log "BLOCKS_SKILLS_NO_UPDATE is set; skipping"
exit 0
fi

# --- locate ourselves ---------------------------------------------------------
# `pwd -P` resolves symlinks, so being invoked through ~/.claude/skills/<name>
# still lands on the real ~/.agents/skills/<name> directory.
script_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd -P) || exit 0
skill_dir=$(cd "$script_dir/.." 2>/dev/null && pwd -P) || exit 0

# The skills CLI reads project locks from the current directory and global
# locks from the home directory. Walk up from the skill until we hit the
# directory that owns the `.agents/` (or `.claude/`) tree; that is where
# `npx skills update` must run so both scopes are considered.
find_root() {
dir=$skill_dir
while [ "$dir" != "/" ] && [ -n "$dir" ]; do
dir=$(dirname "$dir")
if [ -d "$dir/.agents" ] || [ -d "$dir/.claude" ]; then
printf '%s\n' "$dir"
return 0
fi
done
return 1
}

if [ -n "${BLOCKS_SKILLS_ROOT:-}" ]; then
root=$BLOCKS_SKILLS_ROOT
elif ! root=$(find_root); then
root=${HOME:-}
fi
if [ -z "$root" ] || [ ! -d "$root" ]; then
log "could not determine a scope root; skipping"
exit 0
fi
log "skill dir: $skill_dir"
log "scope root: $root"

# --- marker file --------------------------------------------------------------
cache_home=${XDG_CACHE_HOME:-${HOME:-}/.cache}
if [ -z "${XDG_CACHE_HOME:-}" ] && [ -z "${HOME:-}" ]; then
log "neither XDG_CACHE_HOME nor HOME is set; skipping"
exit 0
fi
root_hash=$(printf '%s' "$root" | cksum | cut -d' ' -f1)
marker_dir="$cache_home/blocks-skills/$root_hash"
marker="$marker_dir/$SKILL_NAME"
if ! mkdir -p "$marker_dir" 2>/dev/null; then
log "cannot create $marker_dir; skipping"
exit 0
fi

deadline=${BLOCKS_SKILLS_TIMEOUT:-$UPDATE_TIMEOUT}
case $deadline in
''|*[!0-9]*) deadline=$UPDATE_TIMEOUT ;;
esac

# --- lock ---------------------------------------------------------------------
# Several agent sessions can start at the same moment. `mkdir` is atomic, so
# whoever creates the lock directory reads the marker and runs the update;
# everyone else skips straight away rather than waiting. A lock older than the
# update deadline plus a minute belongs to a run that was killed, so take it
# over.
lock="$marker.lock"
if ! mkdir "$lock" 2>/dev/null; then
stale_after=$(( (deadline + 60 + 59) / 60 )) # minutes, rounded up
if [ -n "$(find "$lock" -maxdepth 0 -mmin +"$stale_after" 2>/dev/null)" ]; then
log "taking over stale lock $lock"
rmdir "$lock" 2>/dev/null
if ! mkdir "$lock" 2>/dev/null; then
log "lost the race for $lock; skipping"
exit 0
fi
else
log "another check holds $lock; skipping"
exit 0
fi
fi
trap 'rmdir "$lock" 2>/dev/null' EXIT

now=${BLOCKS_SKILLS_NOW:-$(date +%s)}
case $now in
''|*[!0-9]*) log "invalid clock value '$now'; skipping"; exit 0 ;;
esac

last_success=0
last_attempt=0
failures=0
if [ -f "$marker" ]; then
while IFS='=' read -r key value; do
case $value in ''|*[!0-9]*) continue ;; esac
case $key in
last_success) last_success=$value ;;
last_attempt) last_attempt=$value ;;
failures) failures=$value ;;
esac
done < "$marker"
fi
log "marker: $marker (last_success=$last_success last_attempt=$last_attempt failures=$failures)"

write_marker() {
tmp="$marker.$$.tmp"
{
printf 'last_success=%s\n' "$last_success"
printf 'last_attempt=%s\n' "$last_attempt"
printf 'failures=%s\n' "$failures"
} > "$tmp" 2>/dev/null && mv -f "$tmp" "$marker" 2>/dev/null
rm -f "$tmp" 2>/dev/null
}

# --- is an update due? --------------------------------------------------------
if [ $((now - last_success)) -lt $SUCCESS_INTERVAL ]; then
log "checked successfully $((now - last_success))s ago; not due"
exit 0
fi

backoff=$RETRY_INTERVAL
if [ "$failures" -ge $MAX_FAILURES ]; then
backoff=$SUCCESS_INTERVAL
fi
if [ $((now - last_attempt)) -lt $backoff ]; then
log "last attempt was $((now - last_attempt))s ago (backoff ${backoff}s, failures=$failures); not due"
exit 0
fi

# --- run the update -----------------------------------------------------------
fingerprint() {
(
cd "$skill_dir" 2>/dev/null || exit 0
for f in SKILL.md references/*.md scripts/*.sh; do
[ -f "$f" ] && cksum "$f"
done | cksum
)
}

update_cmd=${BLOCKS_SKILLS_UPDATE_CMD:-"npx --yes skills@$SKILLS_CLI_VERSION update $SKILL_NAME -y"}
if [ -z "${BLOCKS_SKILLS_UPDATE_CMD:-}" ] && ! command -v npx >/dev/null 2>&1; then
log "npx not found on PATH; skipping"
exit 0
fi

# Run a command with a deadline using only bash builtins, so the guarantee
# holds on systems without coreutils' `timeout` (stock macOS, for one).
# Job control puts the child and the watchdog in their own process groups,
# which lets us take down the whole npx/node tree rather than just the
# wrapping shell. Returns the command's exit code, or 124 on the deadline.
run_with_deadline() { # run_with_deadline <seconds> <output-file> <command>
set -m
( cd "$root" && exec sh -c "$3" ) > "$2" 2>&1 </dev/null &
child=$!
(
sleep "$1"
: > "$2.timeout"
kill -TERM -- "-$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null
sleep 1
kill -KILL -- "-$child" 2>/dev/null || kill -KILL "$child" 2>/dev/null
) >/dev/null 2>&1 </dev/null &
watchdog=$!
set +m
wait "$child" 2>/dev/null
status=$?
kill -KILL -- "-$watchdog" 2>/dev/null || kill -KILL "$watchdog" 2>/dev/null
wait "$watchdog" 2>/dev/null
if [ -f "$2.timeout" ]; then
rm -f "$2.timeout" 2>/dev/null
return 124
fi
return "$status"
}

last_attempt=$now
write_marker
Comment thread
cubeorgdev[bot] marked this conversation as resolved.

before=$(fingerprint)
log "running: $update_cmd (in $root, deadline ${deadline}s)"
output_file="$marker.$$.out"
run_with_deadline "$deadline" "$output_file" "$update_cmd"
rc=$?
output=$(cat "$output_file" 2>/dev/null)
rm -f "$output_file" 2>/dev/null
after=$(fingerprint)
if [ "$rc" -eq 124 ]; then
log "update killed after ${deadline}s"
else
log "update exit code: $rc"
fi
if [ -n "${BLOCKS_SKILLS_DEBUG:-}" ]; then
esc=$(printf '\033')
printf '%s\n' "$output" | sed "s/$esc\[[0-9;?]*[a-zA-Z]//g; s/^/skill-freshness-check: | /" >&2
fi

# The CLI exits 0 for most of its own error paths, so look at the output too.
failed=0
if [ "$rc" -ne 0 ]; then
failed=1
elif printf '%s\n' "$output" | grep -qiE 'failed|cannot update|npm err|error'; then
failed=1
fi

if [ "$failed" -eq 1 ]; then
failures=$((failures + 1))
write_marker
log "update failed (failures=$failures)"
exit 0
fi

last_success=$now
failures=0
write_marker

if [ "$before" != "$after" ]; then
log "skill files changed"
printf 'Note: the %s skill was refreshed to the latest published version. The updated instructions take effect in your next session.\n' "$SKILL_NAME"
else
log "skill already up to date"
fi
exit 0
Loading
Loading