
Github Activity
- 9 installs
- 76 repo stars
- Updated July 31, 2026
- basecamp/house-skills
github-activity is a Claude Code skill that fetches GitHub PRs, reviews, issues, and commits into day-cached JSON atoms.
About
github-activity is a Claude Code skill that fetches GitHub activity - PRs authored, PRs reviewed, issues, and commit contributions - into per-day cached JSON atoms using the gh CLI. A developer uses it as part of the recap plugin's fetcher pipeline to build activity digests. It is idempotent, day-cached, and rate-limit aware.
- Fetches GitHub PRs, reviews, issues, and commits into day-cached JSON atoms
- Uses the gh CLI with --paginate and built-in rate-limit backoff
- Feeds the recap plugin's activity fetcher pipeline
Github Activity by the numbers
- 9 all-time installs (skills.sh)
- Ranked #1,495 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
github-activity capabilities & compatibility
- Capabilities
- github activity fetch · data caching
- Works with
- github
- Use cases
- data analysis
What github-activity says it does
Fetch GitHub PRs, reviews, issues, and commits into day-cached JSON atoms. Uses the gh CLI for authentication and API calls.
Fetch GitHub activity — PRs authored, PRs reviewed, issues created, commit contributions — into per-day cached JSON atoms
npx skills add https://github.com/basecamp/house-skills --skill github-activityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 76 |
| Last updated | July 31, 2026 |
| Repository | basecamp/house-skills ↗ |
What it does
Fetch GitHub PRs, reviews, issues, and commits into day-cached JSON atoms for recap digests.
Who is it for?
Caching GitHub contribution activity into per-day atoms for recap digests.
When should I use this skill?
You need GitHub PRs, reviews, issues, or commits for a recap.
What you get
Per-day cached JSON atoms of GitHub activity ready for digest synthesis.
- day-cached activity.json atoms
By the numbers
- caches PRs authored, PRs reviewed, issues, and commit contributions
Files
GitHub Activity Fetcher
Fetch GitHub activity — PRs authored, PRs reviewed, issues created, commit contributions — into per-day cached JSON atoms at ~/.cache/recap/github/{user}/{YYYY-MM-DD}/activity.json.
Invocation
/recap:github-activity --since 2026-03-23
/recap:github-activity --since 2026-03-23 --until 2026-03-30
/recap:github-activity --user jeremy --org basecamp --since 2026-03-23 --reuseContract
- Idempotent: same input produces same output, safe to re-run
- Day-cached: one JSON file per user per day
- `--reuse`: skip fetch if cache exists and is marked complete
- Rate-limit aware: uses
gh api --paginatewith built-in backoff
Quick Run
# 1. Validate auth
gh auth status || { echo "Run: gh auth login"; exit 1; }
# 2. Determine date range
SINCE=$(date -d "7 days ago" +%Y-%m-%d)
UNTIL=$(date +%Y-%m-%d)
# 3. Run the fetcher
"$SKILL_DIR/scripts/github-activity.sh" --since "$SINCE" --until "$UNTIL"
# 4. Verify cache
USER=$(gh api user --jq '.login')
ls ~/.cache/recap/github/$USER/
cat ~/.cache/recap/github/$USER/$SINCE/activity.json | jq '.metadata'Where $SKILL_DIR = directory containing this SKILL.md.
Output Format
Each activity.json contains:
{
"user": "jeremy",
"date": "2026-03-24",
"prs_authored": [
{
"title": "Add weekly digest skill",
"html_url": "https://github.com/basecamp/coworker/pull/99",
"created_at": "2026-03-24T14:30:00Z",
"repository_url": "https://api.github.com/repos/basecamp/coworker",
"state": "closed",
"pull_request": { "merged_at": "2026-03-24T15:00:00Z" }
}
],
"prs_reviewed": [],
"issues": [],
"metadata": {
"complete": true,
"counts": { "prs_authored": 1, "prs_reviewed": 0, "issues": 0 }
}
}Commit contributions are cached separately as an aggregate at ~/.cache/recap/github/{user}/commits-{since}-{until}.json since GitHub's GraphQL API returns them as a period summary, not per-day.
Arguments
| Argument | Required | Description |
|---|---|---|
--since | Yes | Start date (YYYY-MM-DD) |
--until | No | End date (default: today) |
--user | No | GitHub username (default: authenticated user) |
--org | No | Filter to a specific GitHub org |
--reuse | No | Skip fetch if cache exists and is complete |
Cache Structure
~/.cache/recap/github/
jeremy/
2026-03-24/activity.json
2026-03-25/activity.json
commits-2026-03-23-2026-03-30.jsonPrerequisites
ghCLI installed and authenticated (gh auth login)jqfor JSON parsing
Failure Modes
| Symptom | Cause | Fix |
|---|---|---|
| "gh: not logged in" | Token expired | gh auth login |
| Empty results | No activity in range | Expected — empty days cached as complete |
| Rate limit (403) | Too many API calls | Wait ~1hr and retry |
| >1000 PRs | Search API limit | Narrow date range or add --org filter |
#!/usr/bin/env bash
#
# GitHub activity fetcher — PRs, reviews, commits, day-cached.
#
# Uses the `gh` CLI REST search API (not gh search, which silently drops
# private org repos). Handles >1000 results by splitting by state.
# Caches per day at ~/.cache/recap/github/{user}/{YYYY-MM-DD}/activity.json
#
# Usage:
# ./github-activity.sh --since DATE --until DATE [--user USERNAME] [--org ORG] [--reuse]
#
set -euo pipefail
SINCE_DATE=""
UNTIL_DATE=""
USERNAME=""
ORG=""
REUSE=false
while [[ $# -gt 0 ]]; do
case $1 in
--since) SINCE_DATE="$2"; shift 2 ;;
--until) UNTIL_DATE="$2"; shift 2 ;;
--user) USERNAME="$2"; shift 2 ;;
--org) ORG="$2"; shift 2 ;;
--reuse) REUSE=true; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
if [[ -z "$SINCE_DATE" ]]; then
echo "Error: --since DATE is required" >&2
exit 1
fi
[[ -z "$UNTIL_DATE" ]] && UNTIL_DATE=$(date -u +%Y-%m-%d)
[[ -z "$USERNAME" ]] && USERNAME=$(gh api user --jq '.login')
SINCE_DAY="${SINCE_DATE:0:10}"
UNTIL_DAY="${UNTIL_DATE:0:10}"
SEARCH_RANGE="${SINCE_DAY}..${UNTIL_DAY}"
CACHE_BASE="$HOME/.cache/recap/github/$USERNAME"
[[ -n "$ORG" ]] && CACHE_BASE="$CACHE_BASE/org-$ORG"
echo "Fetching GitHub activity for $USERNAME ($SINCE_DAY to $UNTIL_DAY)..." >&2
days_in_range() {
local current="$1" end="$2"
while [[ "$current" < "$end" || "$current" == "$end" ]]; do
echo "$current"
current=$(date -d "$current + 1 day" +%Y-%m-%d 2>/dev/null || date -j -v+1d -f "%Y-%m-%d" "$current" +%Y-%m-%d)
done
}
# Check if all day caches are already complete (skip API calls with --reuse)
if [[ "$REUSE" == "true" ]]; then
ALL_COMPLETE=true
for day in $(days_in_range "$SINCE_DAY" "$UNTIL_DAY"); do
if ! jq -e '.metadata.complete == true' "$CACHE_BASE/$day/activity.json" >/dev/null 2>&1; then
ALL_COMPLETE=false
break
fi
done
if [[ "$ALL_COMPLETE" == "true" ]]; then
echo " All days cached and complete, skipping fetch" >&2
echo '{"status":"complete","cache_base":"'"$CACHE_BASE"'","reused":true}'
exit 0
fi
fi
ALL_DIR=$(mktemp -d)
# ─────────────────────────────────────────────────
# Helper: paginated search with >1000-result overflow handling
# ─────────────────────────────────────────────────
# Uses REST search API via `gh api search/issues` with -f q= for proper
# URL encoding. Splits by state (merged/closed/open) when a single query
# hits GitHub's 1000-result hard cap.
#
# Usage: gh_search_all "full search query including type and date qualifiers"
# Writes JSON array to stdout.
#
gh_search_all() {
local tmpdir
tmpdir=$(mktemp -d)
local query="$1"
# Paginated fetch via REST search API
if ! gh api "search/issues" --method GET -f q="$query" -f per_page=100 \
--paginate --jq '.items' > "$tmpdir/raw.jsonl" 2>"$tmpdir/stderr.txt"; then
echo " ERROR: GitHub search failed ($(cat "$tmpdir/stderr.txt" | head -1))" >&2
rm -rf "$tmpdir"
return 1
fi
jq -s 'add // []' "$tmpdir/raw.jsonl" > "$tmpdir/all.json"
local count
count=$(jq 'length' "$tmpdir/all.json")
if [[ "$count" -ge 1000 ]]; then
echo " (Hit 1000-result cap, splitting by state...)" >&2
local states
if [[ "$query" == *"is:pr"* ]]; then
states="merged closed open"
else
states="closed open"
fi
for state in $states; do
if ! gh api "search/issues" --method GET \
-f q="${query} is:${state}" -f per_page=100 \
--paginate --jq '.items' > "$tmpdir/state_${state}.raw" 2>"$tmpdir/state_${state}.err"; then
echo " ERROR: state=$state search failed ($(head -1 "$tmpdir/state_${state}.err"))" >&2
rm -rf "$tmpdir"
return 1
fi
jq -s 'add // []' "$tmpdir/state_${state}.raw" > "$tmpdir/state_${state}.json"
rm -f "$tmpdir/state_${state}.raw"
local sc
sc=$(jq 'length' "$tmpdir/state_${state}.json")
echo " state=$state: $sc" >&2
if [[ "$sc" -ge 1000 ]]; then
echo " ERROR: state=$state still at 1000 cap — narrow date range" >&2
rm -rf "$tmpdir"
return 1
fi
done
jq -s 'add | unique_by(.html_url)' "$tmpdir"/state_*.json > "$tmpdir/all.json"
count=$(jq 'length' "$tmpdir/all.json")
fi
cat "$tmpdir/all.json"
rm -rf "$tmpdir"
echo " $count" >&2
}
# ── PRs authored ──
echo " PRs authored..." >&2
QUERY="is:pr created:$SEARCH_RANGE author:$USERNAME"
[[ -n "$ORG" ]] && QUERY="$QUERY org:$ORG"
gh_search_all "$QUERY" > "$ALL_DIR/prs-authored.json"
PRS_COUNT=$(jq 'length' "$ALL_DIR/prs-authored.json")
# ── PRs reviewed — use updated: to capture reviews on older PRs ──
echo " PRs reviewed..." >&2
QUERY="is:pr updated:$SEARCH_RANGE reviewed-by:$USERNAME"
[[ -n "$ORG" ]] && QUERY="$QUERY org:$ORG"
gh_search_all "$QUERY" > "$ALL_DIR/prs-reviewed.json"
REVIEWED_COUNT=$(jq 'length' "$ALL_DIR/prs-reviewed.json")
# ── Issues ──
echo " Issues..." >&2
QUERY="is:issue created:$SEARCH_RANGE author:$USERNAME"
[[ -n "$ORG" ]] && QUERY="$QUERY org:$ORG"
gh_search_all "$QUERY" > "$ALL_DIR/issues.json"
ISSUES_COUNT=$(jq 'length' "$ALL_DIR/issues.json")
# ── Commits (via GraphQL contributions) ──
echo " Commits..." >&2
SINCE_ISO="${SINCE_DAY}T00:00:00Z"
UNTIL_ISO="${UNTIL_DAY}T23:59:59Z"
gh api graphql -f query='
query($login: String!, $from: DateTime!, $to: DateTime!) {
user(login: $login) {
contributionsCollection(from: $from, to: $to) {
totalCommitContributions
commitContributionsByRepository(maxRepositories: 100) {
repository { nameWithOwner }
contributions(first: 1) { totalCount }
}
}
}
}
' -f login="$USERNAME" -f from="$SINCE_ISO" -f to="$UNTIL_ISO" 2>/dev/null | \
jq '.data.user.contributionsCollection' > "$ALL_DIR/commits.json"
if [[ -n "$ORG" ]]; then
# When org-scoped, sum only repos in that org instead of global total
COMMIT_COUNT=$(jq --arg org "$ORG" '
[.commitContributionsByRepository[]
| select(.repository.nameWithOwner | startswith($org + "/"))
| .contributions.totalCount] | add // 0
' "$ALL_DIR/commits.json")
else
COMMIT_COUNT=$(jq '.totalCommitContributions // 0' "$ALL_DIR/commits.json")
fi
echo " $COMMIT_COUNT commits" >&2
# ── Split into per-day cache atoms ──
for day in $(days_in_range "$SINCE_DAY" "$UNTIL_DAY"); do
CACHE_DIR="$CACHE_BASE/$day"
CACHE_FILE="$CACHE_DIR/activity.json"
if [[ "$REUSE" == "true" && -f "$CACHE_FILE" ]]; then
if jq -e '.metadata.complete == true' "$CACHE_FILE" >/dev/null 2>&1; then
continue
fi
fi
mkdir -p "$CACHE_DIR"
NEXT_DAY=$(date -d "$day + 1 day" +%Y-%m-%d 2>/dev/null || date -j -v+1d -f "%Y-%m-%d" "$day" +%Y-%m-%d)
# Filter each category to this day
jq -n \
--arg day "$day" \
--arg next "$NEXT_DAY" \
--arg user "$USERNAME" \
--slurpfile authored "$ALL_DIR/prs-authored.json" \
--slurpfile reviewed "$ALL_DIR/prs-reviewed.json" \
--slurpfile issues "$ALL_DIR/issues.json" '
def day_filter: [.[] | select(.created_at >= ($day + "T00:00:00Z") and .created_at < ($next + "T00:00:00Z"))];
{
user: $user,
date: $day,
prs_authored: ($authored[0] | day_filter),
prs_reviewed: ($reviewed[0] | [.[] | select((.updated_at // .created_at) >= ($day + "T00:00:00Z") and (.updated_at // .created_at) < ($next + "T00:00:00Z"))]),
issues: ($issues[0] | day_filter),
metadata: { complete: true }
}
' > "$CACHE_FILE"
# Add counts
jq '.metadata.counts = {
prs_authored: (.prs_authored | length),
prs_reviewed: (.prs_reviewed // [] | length),
issues: (.issues // [] | length)
}' "$CACHE_FILE" > "${CACHE_FILE}.tmp" && mv "${CACHE_FILE}.tmp" "$CACHE_FILE"
done
# Also cache the commits summary (not day-split — GitHub returns aggregate)
COMMITS_CACHE="$CACHE_BASE/commits-${SINCE_DAY}-${UNTIL_DAY}.json"
cp "$ALL_DIR/commits.json" "$COMMITS_CACHE"
rm -rf "$ALL_DIR"
echo '{"status":"complete","cache_base":"'"$CACHE_BASE"'","prs_authored":'"$PRS_COUNT"',"prs_reviewed":'"$REVIEWED_COUNT"',"issues":'"$ISSUES_COUNT"',"commits":'"$COMMIT_COUNT"'}'