diff --git a/README.md b/README.md index f6d3d4d..27cb06b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is built for local agent-heavy development setups with tools such as Codex, C - Groups known MCP/helper processes by owner and type, with per-process debug output behind `AGENTWATCH_SHOW_HELPERS=1`. - Shows installed coding CLIs and versions when they are discoverable on `PATH`. - Supports cached update checks for npm-distributed CLIs; checks are spaced by `AGENTWATCH_UPDATE_TTL_SECONDS`. -- Shows the Agent Watch plugin version and, when installed from a git checkout, an update action. +- Shows the Agent Watch plugin version and whether release installs are current or have a newer release available. - Hides process command lines by default. Optional redacted command output can be enabled with `AGENTWATCH_SHOW_COMMANDS=1`. - Uses only local process inspection and local HTTP endpoints by default. @@ -54,7 +54,7 @@ cd agent-watch ./scripts/install-dev-swiftbar.sh "$HOME/SwiftBarPlugins" ``` -Release installs show `Update to latest release`, so normal users do not need git. Source checkout installs show the current branch and commit for diagnostics, but hide the menu updater to avoid overwriting checkout-managed files. Development updates should use normal git commands in the checkout. +Release installs show whether the installed plugin is current or whether a newer release is available. `Update to latest release` runs in the background, writes to `AGENTWATCH_UPDATE_LOG`, and replaces the plugin with the latest release asset. Source checkout installs show the current branch and commit for diagnostics, but hide the menu updater to avoid overwriting checkout-managed files. Development updates should use normal git commands in the checkout. ### Linux @@ -104,6 +104,9 @@ The plugin also supports a local config file at `~/.config/agent-watch/config.en | `AGENTWATCH_REPO_URL` | `https://github.com/flamerged/agent-watch` | Project page opened from the menu | | `AGENTWATCH_RELEASE_ASSET_URL` | `https://github.com/flamerged/agent-watch/releases/latest/download/agent-watch.30s.sh` | HTTPS latest release asset URL used by copied-plugin updates | | `AGENTWATCH_UPDATE_LOG` | `$HOME/.cache/agent-watch/update.log` | Update log path | +| `AGENTWATCH_CHECK_RELEASE_UPDATES` | `1` | Set to `0` to disable cached plugin release checks | +| `AGENTWATCH_RELEASE_CHECK_TTL_SECONDS` | `86400` | Minimum seconds between automatic latest-release checks | +| `AGENTWATCH_RELEASE_CHECK_CACHE` | `$HOME/.cache/agent-watch/release-check.tsv` | Latest-release check cache path | | `AGENTWATCH_INTERESTING_PORTS` | `8000,11434,3000,4000,5000` | Comma-separated TCP listening ports to show | ## Privacy And Security @@ -114,9 +117,9 @@ By default it inspects the local process table with `ps`, working directories an Process command lines are hidden by default because they can contain sensitive arguments. If `AGENTWATCH_SHOW_COMMANDS=1` is enabled, commands are still passed through a redactor for common API keys, bearer tokens, GitHub tokens, Anthropic/OpenAI-style keys, Gemini keys, Slack tokens, and Context7 tokens. -Update checks are enabled by default. Agent Watch uses package registry lookups only to refresh a local cache, not on every menu refresh. +Update checks are enabled by default. Agent Watch uses package registry and GitHub release lookups only to refresh local caches, not on every menu refresh. -The default update-check TTL is one day. Set `AGENTWATCH_UPDATE_TTL_SECONDS` to adjust that interval, or use the menu action to refresh the cache manually. +The default update-check TTLs are one day. Set `AGENTWATCH_UPDATE_TTL_SECONDS` for CLI package checks and `AGENTWATCH_RELEASE_CHECK_TTL_SECONDS` for plugin release checks, or use the menu actions to refresh the caches manually. The "Watched Local Ports" section is controlled by `AGENTWATCH_INTERESTING_PORTS`. By default it watches the configured oMLX port, the configured Ollama port, and common local development ports `3000`, `4000`, and `5000`. diff --git a/bin/agent-watch.30s.sh b/bin/agent-watch.30s.sh index a6c966d..7a60372 100755 --- a/bin/agent-watch.30s.sh +++ b/bin/agent-watch.30s.sh @@ -19,6 +19,9 @@ # string(AGENTWATCH_REPO_URL="https://github.com/flamerged/agent-watch"): Agent Watch repository URL # string(AGENTWATCH_RELEASE_ASSET_URL="https://github.com/flamerged/agent-watch/releases/latest/download/agent-watch.30s.sh"): Latest release asset URL for copied-plugin updates # string(AGENTWATCH_UPDATE_LOG="~/.cache/agent-watch/update.log"): Update log file path +# boolean(AGENTWATCH_CHECK_RELEASE_UPDATES=true): Check latest Agent Watch release in the background +# string(AGENTWATCH_RELEASE_CHECK_TTL_SECONDS="86400"): Seconds between latest-release checks when enabled +# string(AGENTWATCH_RELEASE_CHECK_CACHE="~/.cache/agent-watch/release-check.tsv"): Latest-release check cache path # string(AGENTWATCH_INTERESTING_PORTS="8000,11434,3000,4000,5000"): TCP ports to show # Agent Watch # v0.3.0 # x-release-please-version @@ -78,6 +81,8 @@ write_default_config() { builtin print -r -- "# CLI update checks are cached and spaced by this TTL." builtin print -r -- "AGENTWATCH_CHECK_UPDATES=true" builtin print -r -- "AGENTWATCH_UPDATE_TTL_SECONDS=86400" + builtin print -r -- "AGENTWATCH_CHECK_RELEASE_UPDATES=true" + builtin print -r -- "AGENTWATCH_RELEASE_CHECK_TTL_SECONDS=86400" builtin print -r -- "" builtin print -r -- "# Uncomment these when you want extra debug or local actions." builtin print -r -- "# AGENTWATCH_SHOW_HELPERS=true" @@ -165,6 +170,10 @@ AGENTWATCH_REPO_URL="${AGENTWATCH_REPO_URL:-https://github.com/flamerged/agent-w RELEASE_ASSET_URL="${AGENTWATCH_RELEASE_ASSET_URL:-https://github.com/flamerged/agent-watch/releases/latest/download/agent-watch.30s.sh}" UPDATE_LOG="${AGENTWATCH_UPDATE_LOG:-$HOME/.cache/agent-watch/update.log}" UPDATE_LOG="${UPDATE_LOG/#\~/$HOME}" +CHECK_RELEASE_UPDATES="${AGENTWATCH_CHECK_RELEASE_UPDATES:-1}" +RELEASE_CHECK_TTL_SECONDS="${AGENTWATCH_RELEASE_CHECK_TTL_SECONDS:-86400}" +RELEASE_CHECK_CACHE="${AGENTWATCH_RELEASE_CHECK_CACHE:-$HOME/.cache/agent-watch/release-check.tsv}" +RELEASE_CHECK_CACHE="${RELEASE_CHECK_CACHE/#\~/$HOME}" AGENTMEMORY_LOG="${AGENTWATCH_AGENTMEMORY_LOG:-$HOME/local-agentmemory/logs/agentmemory.log}" OMLX_URL="$(strip_slash "${AGENTWATCH_OMLX_URL:-http://127.0.0.1:8000}")" OLLAMA_URL="$(strip_slash "${AGENTWATCH_OLLAMA_URL:-http://127.0.0.1:11434}")" @@ -248,6 +257,153 @@ plugin_version_label() { emit "v${PLUGIN_VERSION}" } +file_mtime() { + local file="$1" + [[ -f "$file" ]] || { emit ""; return; } + if stat -f %m "$file" >/dev/null 2>&1; then + stat -f %m "$file" + elif stat -c %Y "$file" >/dev/null 2>&1; then + stat -c %Y "$file" + fi +} + +release_tag_norm() { + emit "${1%%-*}" | "$SED" 's/^v//' +} + +latest_release_tag_from_asset_url() { + local tag + tag="$(emit "$RELEASE_ASSET_URL" | "$SED" -n 's#.*releases/download/\([^/]*\)/.*#\1#p' | head -1)" + [[ -n "$tag" && "$tag" != "latest" ]] || return 1 + emit "$tag" +} + +github_repo_slug() { + local slug + [[ "$AGENTWATCH_REPO_URL" == https://github.com/* ]] || return 1 + slug="${AGENTWATCH_REPO_URL#https://github.com/}" + slug="${slug%%\?*}" + slug="${slug%%#*}" + slug="${slug%/}" + slug="${slug%.git}" + [[ "$slug" == */* && "$slug" != */*/* ]] || return 1 + emit "$slug" +} + +latest_release_tag() { + local repo tag + have "$CURL" || return 1 + if repo="$(github_repo_slug)" && [[ -n "$repo" ]]; then + tag="$("$CURL" -fsSL \ + --connect-timeout 5 \ + --max-time 15 \ + --retry 1 \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null \ + | "$JQ" -r '.tag_name // empty' 2>/dev/null)" + if [[ -n "$tag" && "$tag" != "null" ]]; then + emit "$tag" + return + fi + fi + latest_release_tag_from_asset_url +} + +write_release_check_cache() { + local now tmp latest check_status rc + now="$(date +%s 2>/dev/null || emit "0")" + mkdir -p "${RELEASE_CHECK_CACHE:h}" 2>/dev/null || { + rm -f "${RELEASE_CHECK_CACHE}.lock" 2>/dev/null || true + return 1 + } + tmp="${RELEASE_CHECK_CACHE}.$$" + if latest="$(latest_release_tag)"; then + check_status="ok" + else + check_status="error" + latest="" + fi + if builtin printf '%s\t%s\t%s\n' "$now" "$check_status" "$latest" > "$tmp" \ + && mv "$tmp" "$RELEASE_CHECK_CACHE"; then + [[ "$check_status" == "ok" ]] + rc=$? + else + rm -f "$tmp" 2>/dev/null || true + rc=1 + fi + rm -f "${RELEASE_CHECK_CACHE}.lock" 2>/dev/null || true + return "$rc" +} + +release_check_cache_fields() { + [[ -f "$RELEASE_CHECK_CACHE" ]] || return 1 + local ts check_status latest rest + IFS=$'\t' read -r ts check_status latest rest < "$RELEASE_CHECK_CACHE" || return 1 + builtin printf '%s\t%s\t%s\n' "$ts" "$check_status" "$latest" +} + +release_check_cache_age() { + local mtime now + mtime="$(file_mtime "$RELEASE_CHECK_CACHE")" + [[ -n "$mtime" ]] || { emit ""; return; } + now="$(date +%s 2>/dev/null || emit "0")" + emit $(( now - mtime )) +} + +maybe_refresh_release_check() { + truthy "$CHECK_RELEASE_UPDATES" || return + [[ "$RELEASE_CHECK_TTL_SECONDS" == <-> ]] || RELEASE_CHECK_TTL_SECONDS=86400 + local age lock_file lock_mtime current_mtime now lock_age + age="$(release_check_cache_age)" + if [[ -n "$age" && "$age" -le "$RELEASE_CHECK_TTL_SECONDS" ]]; then + return + fi + mkdir -p "${RELEASE_CHECK_CACHE:h}" 2>/dev/null || return + lock_file="${RELEASE_CHECK_CACHE}.lock" + lock_mtime="$(file_mtime "$lock_file")" + if [[ -n "$lock_mtime" ]]; then + now="$(date +%s 2>/dev/null || emit "0")" + lock_age=$(( now - lock_mtime )) + if [[ "$lock_age" -gt 300 ]]; then + current_mtime="$(file_mtime "$lock_file")" + [[ "$current_mtime" == "$lock_mtime" ]] && rm -f "$lock_file" 2>/dev/null || true + fi + fi + ( set -C; : > "$lock_file" ) 2>/dev/null || return + "$PLUGIN_PATH" check-release >/dev/null 2>&1 & +} + +release_status_label() { + local current="$1" fields ts check_status latest + truthy "$CHECK_RELEASE_UPDATES" || { emit "update checks disabled"; return; } + fields="$(release_check_cache_fields)" || { emit "checking latest release"; return; } + IFS=$'\t' read -r ts check_status latest <<< "$fields" + if [[ "$check_status" != "ok" || -z "$latest" ]]; then + emit "latest check unavailable" + elif [[ "$(release_tag_norm "$latest")" == "$(release_tag_norm "$current")" ]]; then + emit "latest" + else + emit "update $latest available" + fi +} + +release_status_color() { + local label="$1" + case "$label" in + update\ *\ available) emit "#cc6633" ;; + latest) emit "#2f8f46" ;; + *) emit "#888888" ;; + esac +} + +cached_latest_release_tag() { + local fields ts check_status latest + fields="$(release_check_cache_fields)" || return 1 + IFS=$'\t' read -r ts check_status latest <<< "$fields" + [[ "$check_status" == "ok" && -n "$latest" ]] || return 1 + emit "$latest" +} + log_update() { mkdir -p "${UPDATE_LOG:h}" 2>/dev/null || return builtin print -r -- "[$(date '+%Y-%m-%d %H:%M:%S %z' 2>/dev/null || date)] ${1:-}" >> "$UPDATE_LOG" 2>/dev/null || true @@ -388,6 +544,10 @@ case "$ACTION" in write_update_cache exit 0 ;; + check-release) + write_release_check_cache + exit $? + ;; configure) write_default_config && open_text_target "$CONFIG_FILE" exit $? @@ -971,10 +1131,17 @@ open_ports_rows() { } print_plugin_rows() { - local root git_summary version_label + local root git_summary version_label release_status release_color latest_release root="$(plugin_repo_root)" version_label="$(plugin_version_label "$root")" - emit "--Version: ${version_label} | font=Menlo" + if [[ -z "$root" ]]; then + maybe_refresh_release_check + release_status="$(release_status_label "$version_label")" + release_color="$(release_status_color "$release_status")" + emit "--Version: ${version_label} (${release_status}) | font=Menlo color=$release_color" + else + emit "--Version: ${version_label} | font=Menlo" + fi emit "--Config: $(shorten_path "$CONFIG_FILE") | font=Menlo" emit "--Script: $(shorten_path "$PLUGIN_PATH") | font=Menlo" if [[ -n "$root" ]]; then @@ -983,7 +1150,13 @@ print_plugin_rows() { emit "--Git: ${git_summary:-unknown} | font=Menlo" emit "----Use git commands for development updates | color=gray" else - emit "--Update to latest release | bash=$PLUGIN_PATH param1=update-release terminal=false refresh=true" + latest_release="$(cached_latest_release_tag 2>/dev/null || true)" + if [[ -n "$latest_release" && "$(release_tag_norm "$latest_release")" != "$(release_tag_norm "$version_label")" ]]; then + emit "--Update to $latest_release | bash=$PLUGIN_PATH param1=update-release terminal=false refresh=true" + else + emit "--Update to latest release | bash=$PLUGIN_PATH param1=update-release terminal=false refresh=true" + fi + emit "--Check plugin update status now | bash=$PLUGIN_PATH param1=check-release terminal=false refresh=true" fi [[ -f "$UPDATE_LOG" ]] && emit "--Open update log | bash=$PLUGIN_PATH param1=open param2=update-log terminal=false" emit "--Open config file | bash=$PLUGIN_PATH param1=open param2=agent-watch-config terminal=false refresh=true" diff --git a/scripts/check.sh b/scripts/check.sh index 32d8787..47687e0 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -35,6 +35,9 @@ for var in \ 'AGENTWATCH_REPO_URL' \ 'AGENTWATCH_RELEASE_ASSET_URL' \ 'AGENTWATCH_UPDATE_LOG' \ + 'AGENTWATCH_CHECK_RELEASE_UPDATES' \ + 'AGENTWATCH_RELEASE_CHECK_TTL_SECONDS' \ + 'AGENTWATCH_RELEASE_CHECK_CACHE' \ 'AGENTWATCH_INTERESTING_PORTS'; do grep -q ".*$var" "$PLUGIN" done @@ -43,6 +46,7 @@ output="$( AGENTWATCH_SHOW_COMMANDS=0 \ AGENTWATCH_CONFIG_FILE="$(mktemp)" \ AGENTWATCH_CHECK_UPDATES=0 \ + AGENTWATCH_CHECK_RELEASE_UPDATES=0 \ AGENTWATCH_OMLX_URL=http://127.0.0.1:1 \ AGENTWATCH_OLLAMA_URL=http://127.0.0.1:1 \ AGENTWATCH_AGENTMEMORY_URL=http://127.0.0.1:1 \ @@ -63,6 +67,7 @@ print -r -- 'AGENTWATCH_INTERESTING_PORTS=54321' > "$config_file" config_output="$( AGENTWATCH_CONFIG_FILE="$config_file" \ AGENTWATCH_CHECK_UPDATES=0 \ + AGENTWATCH_CHECK_RELEASE_UPDATES=0 \ AGENTWATCH_OMLX_URL=http://127.0.0.1:1 \ AGENTWATCH_OLLAMA_URL=http://127.0.0.1:1 \ AGENTWATCH_AGENTMEMORY_URL=http://127.0.0.1:1 \