Initial commit: OpenClaw ops workspace
This commit is contained in:
127
skills/github-notifications/SKILL.md
Normal file
127
skills/github-notifications/SKILL.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: github-notifications
|
||||
description: Check GitHub notifications for PR activity and major releases. Filters for PRs where user is mentioned/author, and major releases (v*.0.0) plus all Mirrowel/LLM-API-Key-Proxy dev builds. Tracks state to avoid duplicate alerts. Use for periodic GitHub notification checking via cron jobs or manual checks.
|
||||
---
|
||||
|
||||
# GitHub Notifications Checker
|
||||
|
||||
Efficiently check GitHub notifications with smart filtering and state tracking.
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Fetches notifications** via GitHub CLI (`gh api`)
|
||||
2. **Filters intelligently:**
|
||||
- PRs where you're mentioned, author, or review requested
|
||||
- Major releases (v*.0.0 format)
|
||||
- ALL releases from `Mirrowel/LLM-API-Key-Proxy` (including dev builds)
|
||||
- Excludes: rc/pre/beta/alpha/nightly releases
|
||||
3. **Tracks state** to avoid duplicate notifications
|
||||
4. **Returns JSON** for easy parsing
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Check
|
||||
|
||||
```bash
|
||||
bash skills/github-notifications/scripts/check.sh
|
||||
```
|
||||
|
||||
**Output when nothing new:**
|
||||
```json
|
||||
{"hasNew":false}
|
||||
```
|
||||
|
||||
**Output with new activity:**
|
||||
```json
|
||||
{
|
||||
"hasNew": true,
|
||||
"newPRs": [
|
||||
{
|
||||
"repo": "openclaw/openclaw",
|
||||
"title": "feat: Add cron silent mode",
|
||||
"url": "https://api.github.com/repos/openclaw/openclaw/pulls/1234",
|
||||
"updated": "2026-02-03T14:30:00Z",
|
||||
"reason": "mention",
|
||||
"id": "openclaw/openclaw#feat: Add cron silent mode"
|
||||
}
|
||||
],
|
||||
"newReleases": [
|
||||
{
|
||||
"repo": "some/repo",
|
||||
"title": "v2.0.0",
|
||||
"updated": "2026-02-03T12:00:00Z",
|
||||
"id": "some/repo@v2.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `STATE_FILE` - Path to state tracking file (default: `memory/github-check-state.json`)
|
||||
- `WORKSPACE` - Workspace directory (default: `/home/node/.openclaw/workspace`)
|
||||
|
||||
### State Tracking
|
||||
|
||||
State is stored in `memory/github-check-state.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"lastCheck": "2026-02-03T14:00:00Z",
|
||||
"seenPRs": ["repo#PR Title", ...],
|
||||
"seenReleases": ["repo@v1.0.0", ...]
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Cron
|
||||
|
||||
This skill is designed to work with OpenClaw cron jobs. The script handles all filtering and state management, only calling the LLM when there's actual content to summarize.
|
||||
|
||||
**Recommended cron setup:**
|
||||
|
||||
1. Script runs periodically (every 4 hours)
|
||||
2. If `hasNew: false`, script exits silently - no LLM call, no message
|
||||
3. If `hasNew: true`, cron job can format the summary and deliver it
|
||||
|
||||
This approach:
|
||||
- ✅ Saves tokens (no LLM call when nothing new)
|
||||
- ✅ Handles errors gracefully (GitHub API failures logged)
|
||||
- ✅ Avoids duplicate notifications (state tracking)
|
||||
- ✅ Faster execution (no LLM parsing)
|
||||
|
||||
## Error Handling
|
||||
|
||||
If GitHub API fails, returns:
|
||||
```json
|
||||
{
|
||||
"error": "GitHub API failed",
|
||||
"details": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Check for `.error` field in output to detect failures.
|
||||
|
||||
## Auto-Dismiss Low-Value Notifications
|
||||
|
||||
```bash
|
||||
# Dry run (see what would be dismissed)
|
||||
DRY_RUN=true bash skills/github-notifications/scripts/auto-dismiss.sh
|
||||
|
||||
# Actually dismiss
|
||||
bash skills/github-notifications/scripts/auto-dismiss.sh
|
||||
```
|
||||
|
||||
**Auto-dismisses:**
|
||||
- Title matches: nightly, preview, checkpoint, pre-release, canary, alpha, beta, snapshot
|
||||
- Releases with empty release notes
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{"dismissed":3,"checked":12}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- `gh` CLI authenticated
|
||||
- `jq` for JSON parsing
|
||||
- GitHub token with `notifications` scope
|
||||
74
skills/github-notifications/scripts/auto-dismiss.sh
Executable file
74
skills/github-notifications/scripts/auto-dismiss.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# Auto-dismiss GitHub notifications matching certain patterns
|
||||
# - Nightlies, previews, checkpoints, rc, etc (by title pattern)
|
||||
# - Releases with no release notes
|
||||
# Exempts specified repos from auto-dismiss
|
||||
#
|
||||
# Dismiss = PATCH (read) + DELETE thread + DELETE subscription
|
||||
|
||||
set -e
|
||||
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
|
||||
# Repos exempt from auto-dismiss (always show these)
|
||||
EXEMPT_REPOS="Mirrowel/LLM-API-Key-Proxy|b3nw/LLM-API-Key-Proxy|pedramamini/Maestro"
|
||||
|
||||
# Patterns to auto-dismiss (case-insensitive)
|
||||
DISMISS_PATTERNS="nightly|preview|checkpoint|pre-release|canary|alpha|beta|snapshot|-rc\.|rc[0-9]"
|
||||
|
||||
# Get all unread notifications
|
||||
NOTIFICATIONS=$(gh api /notifications 2>/dev/null || echo "[]")
|
||||
|
||||
if [ "$NOTIFICATIONS" = "[]" ] || [ -z "$NOTIFICATIONS" ]; then
|
||||
echo '{"dismissed":0,"checked":0}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DISMISSED=0
|
||||
TOTAL=$(echo "$NOTIFICATIONS" | jq 'length')
|
||||
|
||||
while read -r notif; do
|
||||
ID=$(echo "$notif" | jq -r '.id')
|
||||
TITLE=$(echo "$notif" | jq -r '.subject.title')
|
||||
TYPE=$(echo "$notif" | jq -r '.subject.type')
|
||||
URL=$(echo "$notif" | jq -r '.subject.url')
|
||||
REPO=$(echo "$notif" | jq -r '.repository.full_name')
|
||||
|
||||
# Skip exempt repos
|
||||
if echo "$REPO" | grep -qiE "$EXEMPT_REPOS"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
SHOULD_DISMISS=false
|
||||
REASON=""
|
||||
|
||||
# Check title patterns
|
||||
if echo "$TITLE" | grep -qiE "$DISMISS_PATTERNS"; then
|
||||
SHOULD_DISMISS=true
|
||||
REASON="title_pattern"
|
||||
fi
|
||||
|
||||
# Check releases with no notes
|
||||
if [ "$TYPE" = "Release" ] && [ "$SHOULD_DISMISS" = "false" ]; then
|
||||
RELEASE_BODY=$(gh api "$URL" --jq '.body // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$RELEASE_BODY" ] || [ "$RELEASE_BODY" = "null" ]; then
|
||||
SHOULD_DISMISS=true
|
||||
REASON="empty_release_notes"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SHOULD_DISMISS" = "true" ]; then
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "Would dismiss: [$REPO] $TITLE ($REASON)" >&2
|
||||
else
|
||||
# Full dismiss: mark read + delete thread + delete subscription
|
||||
gh api -X PATCH "/notifications/threads/$ID" 2>/dev/null || true
|
||||
gh api -X DELETE "/notifications/threads/$ID" 2>/dev/null || true
|
||||
gh api -X DELETE "/notifications/threads/$ID/subscription" 2>/dev/null || true
|
||||
echo "Dismissed: [$REPO] $TITLE ($REASON)" >&2
|
||||
fi
|
||||
DISMISSED=$((DISMISSED + 1))
|
||||
fi
|
||||
done < <(echo "$NOTIFICATIONS" | jq -c '.[]')
|
||||
|
||||
echo "{\"dismissed\":$DISMISSED,\"checked\":$TOTAL}"
|
||||
145
skills/github-notifications/scripts/check.sh
Executable file
145
skills/github-notifications/scripts/check.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# GitHub Notifications Checker
|
||||
# Filters PRs and releases, tracks state, returns JSON summary
|
||||
|
||||
set -e
|
||||
|
||||
STATE_FILE="${STATE_FILE:-memory/github-check-state.json}"
|
||||
WORKSPACE="${WORKSPACE:-/home/node/.openclaw/workspace}"
|
||||
cd "$WORKSPACE"
|
||||
|
||||
# Initialize state file if missing
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
echo '{"lastCheck":"1970-01-01T00:00:00Z","seenPRs":[],"seenReleases":[]}' > "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# Load last check time
|
||||
LAST_CHECK=$(jq -r '.lastCheck' "$STATE_FILE")
|
||||
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Fetch notifications (PRs only)
|
||||
if ! PR_DATA=$(gh api 'notifications?all=true&per_page=100' 2>&1); then
|
||||
echo '{"error":"GitHub API failed","details":"'"${PR_DATA//\"/\\\"}"'"}' | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Filter PRs where user is mentioned/author/review requested/subscribed
|
||||
FILTERED_PRS=$(echo "$PR_DATA" | jq -r '[
|
||||
.[] |
|
||||
select(.subject.type == "PullRequest") |
|
||||
select(.reason == "mention" or .reason == "author" or .reason == "review_requested" or .reason == "subscribed") |
|
||||
{
|
||||
repo: .repository.full_name,
|
||||
title: .subject.title,
|
||||
url: .subject.url,
|
||||
updated: .updated_at,
|
||||
reason: .reason,
|
||||
id: (.repository.full_name + "#" + .subject.title)
|
||||
}
|
||||
]')
|
||||
|
||||
# Filter releases
|
||||
RELEASE_DATA=$(echo "$PR_DATA" | jq -r '[
|
||||
.[] |
|
||||
select(.subject.type == "Release") |
|
||||
{
|
||||
repo: .repository.full_name,
|
||||
title: .subject.title,
|
||||
updated: .updated_at,
|
||||
reason: .reason,
|
||||
id: (.repository.full_name + "@" + .subject.title)
|
||||
}
|
||||
]')
|
||||
|
||||
# Filter releases:
|
||||
# - include subscribed releases (user-requested)
|
||||
# - keep legacy inclusion for major releases + whitelist repos
|
||||
# - ignore dev/pre-release markers for non-whitelist repos
|
||||
# - additionally ignore GitHub prerelease=true for non-whitelist repos
|
||||
# - whitelist repos always pass through
|
||||
FILTERED_RELEASES=$(echo "$RELEASE_DATA" | jq -r '[
|
||||
.[] |
|
||||
. as $r |
|
||||
($r.repo == "Mirrowel/LLM-API-Key-Proxy" or $r.repo == "openclaw/openclaw" or $r.repo == "anomalyco/opencode") as $whitelisted |
|
||||
select(
|
||||
($r.reason == "subscribed") or
|
||||
$whitelisted or
|
||||
($r.title | test("^v[0-9]+\\.0\\.0"))
|
||||
) |
|
||||
select(
|
||||
$whitelisted or
|
||||
(($r.title | ascii_downcase) | test("(rc|pre|beta|alpha|nightly|dev|exp|canary|snapshot)") | not)
|
||||
)
|
||||
]')
|
||||
|
||||
# Enrich release candidates with GitHub prerelease flag (best-effort).
|
||||
# Notifications payload lacks prerelease metadata, so look up each candidate by repo+title.
|
||||
# For non-whitelisted repos, exclude prerelease=true.
|
||||
FILTERED_RELEASES=$(echo "$FILTERED_RELEASES" | jq -c '.[]' | while read -r rel; do
|
||||
repo=$(echo "$rel" | jq -r '.repo')
|
||||
title=$(echo "$rel" | jq -r '.title')
|
||||
|
||||
whitelisted=false
|
||||
if [ "$repo" = "Mirrowel/LLM-API-Key-Proxy" ] || [ "$repo" = "openclaw/openclaw" ] || [ "$repo" = "anomalyco/opencode" ]; then
|
||||
whitelisted=true
|
||||
fi
|
||||
|
||||
# Whitelist bypasses prerelease metadata filtering.
|
||||
if [ "$whitelisted" = "true" ]; then
|
||||
echo "$rel"
|
||||
continue
|
||||
fi
|
||||
|
||||
# URL-encode tag (title) using jq for safety.
|
||||
tag_encoded=$(jq -nr --arg s "$title" '$s|@uri')
|
||||
|
||||
# If lookup fails, keep item (fail-open) to avoid dropping potentially important notifications.
|
||||
prerelease=$(gh api "repos/$repo/releases/tags/$tag_encoded" --jq '.prerelease' 2>/dev/null || echo "lookup_failed")
|
||||
|
||||
if [ "$prerelease" = "true" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$rel"
|
||||
done | jq -s '.')
|
||||
|
||||
# Load seen items
|
||||
SEEN_PRS=$(jq -r '.seenPRs // []' "$STATE_FILE")
|
||||
SEEN_RELEASES=$(jq -r '.seenReleases // []' "$STATE_FILE")
|
||||
|
||||
# Find new items
|
||||
NEW_PRS=$(echo "$FILTERED_PRS" | jq --argjson seen "$SEEN_PRS" '[
|
||||
.[] | select(.id as $id | $seen | index($id) | not)
|
||||
]')
|
||||
|
||||
NEW_RELEASES=$(echo "$FILTERED_RELEASES" | jq --argjson seen "$SEEN_RELEASES" '[
|
||||
.[] | select(.id as $id | $seen | index($id) | not)
|
||||
]')
|
||||
|
||||
# Count new items
|
||||
NEW_PR_COUNT=$(echo "$NEW_PRS" | jq 'length')
|
||||
NEW_RELEASE_COUNT=$(echo "$NEW_RELEASES" | jq 'length')
|
||||
|
||||
# Update state
|
||||
ALL_PR_IDS=$(echo "$FILTERED_PRS" | jq -r '[.[].id]')
|
||||
ALL_RELEASE_IDS=$(echo "$FILTERED_RELEASES" | jq -r '[.[].id]')
|
||||
|
||||
jq -n \
|
||||
--arg now "$NOW" \
|
||||
--argjson prIds "$ALL_PR_IDS" \
|
||||
--argjson relIds "$ALL_RELEASE_IDS" \
|
||||
'{lastCheck:$now, seenPRs:$prIds, seenReleases:$relIds}' \
|
||||
> "$STATE_FILE"
|
||||
|
||||
# Output result
|
||||
if [ "$NEW_PR_COUNT" -eq 0 ] && [ "$NEW_RELEASE_COUNT" -eq 0 ]; then
|
||||
echo '{"hasNew":false}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Return new items
|
||||
jq -n \
|
||||
--argjson prs "$NEW_PRS" \
|
||||
--argjson releases "$NEW_RELEASES" \
|
||||
'{hasNew:true, newPRs:$prs, newReleases:$releases}'
|
||||
98
skills/github-notifications/scripts/cron-wrapper.sh
Executable file
98
skills/github-notifications/scripts/cron-wrapper.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# Cron wrapper for GitHub notifications
|
||||
# 1. Auto-dismisses low-value notifications (nightlies, previews, empty releases)
|
||||
# 2. Checks remaining notifications and formats for human consumption
|
||||
|
||||
set -e
|
||||
|
||||
WORKSPACE="${WORKSPACE:-/home/node/.openclaw/workspace}"
|
||||
cd "$WORKSPACE"
|
||||
|
||||
# First: auto-dismiss low-value notifications
|
||||
bash skills/github-notifications/scripts/auto-dismiss.sh >/dev/null 2>&1 || true
|
||||
|
||||
# Then: run the checker
|
||||
RESULT=$(bash skills/github-notifications/scripts/check.sh)
|
||||
|
||||
# Check for errors
|
||||
if echo "$RESULT" | jq -e '.error' > /dev/null 2>&1; then
|
||||
ERROR_MSG=$(echo "$RESULT" | jq -r '.error')
|
||||
ERROR_DETAILS=$(echo "$RESULT" | jq -r '.details')
|
||||
echo "❌ **GitHub Check Failed**"
|
||||
echo ""
|
||||
echo "Error: $ERROR_MSG"
|
||||
echo "\`\`\`"
|
||||
echo "$ERROR_DETAILS" | head -20
|
||||
echo "\`\`\`"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if there's new activity
|
||||
HAS_NEW=$(echo "$RESULT" | jq -r '.hasNew')
|
||||
|
||||
if [ "$HAS_NEW" != "true" ]; then
|
||||
# Nothing new - stay completely silent (no output = no message)
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Format and output the summary
|
||||
echo "🔔 **GitHub Activity Update**"
|
||||
echo ""
|
||||
|
||||
# Process PRs
|
||||
PR_COUNT=$(echo "$RESULT" | jq '.newPRs | length')
|
||||
if [ "$PR_COUNT" -gt 0 ]; then
|
||||
echo "**Pull Requests ($PR_COUNT new):**"
|
||||
echo "$RESULT" | jq -r '.newPRs[] | "- **\(.repo)** #\(.title)\n Updated: \(.updated) | Reason: \(.reason)"'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Process Releases
|
||||
RELEASE_COUNT=$(echo "$RESULT" | jq '.newReleases | length')
|
||||
if [ "$RELEASE_COUNT" -gt 0 ]; then
|
||||
echo "**Releases ($RELEASE_COUNT new):**"
|
||||
while IFS= read -r rel; do
|
||||
repo=$(echo "$rel" | jq -r '.repo')
|
||||
title=$(echo "$rel" | jq -r '.title')
|
||||
updated=$(echo "$rel" | jq -r '.updated')
|
||||
|
||||
echo "- **$repo** \`$title\`"
|
||||
echo " Released: $updated"
|
||||
|
||||
# Best-effort major changes summary from release body.
|
||||
# Use the release API URL directly from notifications/checker output when available.
|
||||
release_url=$(echo "$rel" | jq -r '.url // empty')
|
||||
body=""
|
||||
|
||||
if [ -n "$release_url" ]; then
|
||||
body=$(gh api "$release_url" --jq '.body' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Fallback only if direct release lookup failed and title might actually equal tag.
|
||||
if [ -z "$body" ] || [ "$body" = "null" ]; then
|
||||
tag_encoded=$(jq -nr --arg s "$title" '$s|@uri')
|
||||
body=$(gh api "repos/$repo/releases/tags/$tag_encoded" --jq '.body' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$body" ] && [ "$body" != "null" ]; then
|
||||
summary=$(printf '%s\n' "$body" \
|
||||
| sed 's/\r$//' \
|
||||
| awk 'NF' \
|
||||
| grep -E '^(\- |\* |[0-9]+\.|## |### )' \
|
||||
| head -3 \
|
||||
| sed 's/^/ /')
|
||||
|
||||
if [ -z "$summary" ]; then
|
||||
summary=$(printf '%s\n' "$body" | awk 'NF{print; exit}' | cut -c1-240)
|
||||
[ -n "$summary" ] && summary=" $summary"
|
||||
fi
|
||||
|
||||
if [ -n "$summary" ]; then
|
||||
echo " Major changes:"
|
||||
echo "$summary"
|
||||
fi
|
||||
else
|
||||
echo " Major changes: release details unavailable from GitHub API."
|
||||
fi
|
||||
done < <(echo "$RESULT" | jq -c '.newReleases[]')
|
||||
fi
|
||||
Reference in New Issue
Block a user