Initial commit: OpenClaw ops workspace

This commit is contained in:
2026-03-28 00:15:47 +00:00
commit f1aeaeefb5
42 changed files with 4297 additions and 0 deletions

View 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

View 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}"

View 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}'

View 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