
Basecamp Activity
- 6 installs
- 76 repo stars
- Updated July 31, 2026
- basecamp/house-skills
basecamp-activity is a Claude Code skill that fetches Basecamp project or person activity into day-cached JSON atoms.
About
basecamp-activity is a Claude Code skill that fetches Basecamp project or person activity into per-day cached JSON atoms via the Basecamp API. A developer uses it as part of the recap plugin's fetcher pipeline to build daily, weekly, or monthly activity digests. It is idempotent, day-cached, and handles pagination and rate-limit backoff.
- Fetches Basecamp project or person activity into day-cached JSON atoms
- Idempotent and rate-limit aware with exponential backoff on 429s
- Feeds the recap plugin's activity fetcher pipeline
Basecamp Activity by the numbers
- 6 all-time installs (skills.sh)
- Ranked #1,691 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
basecamp-activity capabilities & compatibility
- Capabilities
- activity fetch · data caching
- Use cases
- data analysis
What basecamp-activity says it does
Fetch Basecamp project or person activity into day-cached JSON atoms. Uses the Basecamp API with pagination and rate-limit backoff.
Part of the recap plugin's activity fetcher pipeline.
npx skills add https://github.com/basecamp/house-skills --skill basecamp-activityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 76 |
| Last updated | July 31, 2026 |
| Repository | basecamp/house-skills ↗ |
What it does
Fetch Basecamp project or person activity into day-cached JSON atoms for recap digests.
Who is it for?
Pulling Basecamp activity into cached atoms for recap digests.
When should I use this skill?
You need to fetch Basecamp project or person activity for a recap.
What you get
Per-day cached JSON atoms of Basecamp activity ready for digest synthesis.
- day-cached activity.json atoms
By the numbers
- one JSON file per scope per day
Files
Basecamp Activity Fetcher
Fetch Basecamp activity events into per-day cached JSON atoms. Supports two scopes:
- Project-scoped (
--project): all events in a Basecamp project - Person-scoped (
--person): all activity by a specific user
Caches at ~/.cache/recap/basecamp-project/{id}/{YYYY-MM-DD}/activity.json or ~/.cache/recap/basecamp-person/{slug}/{YYYY-MM-DD}/activity.json.
Invocation
/recap:basecamp-activity --project 43483623 --since 2026-03-23
/recap:basecamp-activity --project 43483623 --since 2026-03-23 --until 2026-03-30 --reuse
/recap:basecamp-activity --person "Jeremy Daer" --since 2026-03-23Contract
- Idempotent: same input produces same output, safe to re-run
- Day-cached: one JSON file per scope per day
- `--reuse`: skip fetch if cache exists and is marked complete
- Rate-limit aware: exponential backoff on 429 responses
Quick Run
# 1. Validate auth
basecamp auth status | jq -e '.data.authenticated' || { echo "Run: basecamp 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 (project-scoped)
"$SKILL_DIR/scripts/basecamp-activity.sh" \
--project 43483623 --since "$SINCE" --until "$UNTIL"
# 4. Verify cache
ls ~/.cache/recap/basecamp-project/43483623/
cat ~/.cache/recap/basecamp-project/43483623/$SINCE/activity.json | jq '.metadata'Where $SKILL_DIR = directory containing this SKILL.md.
Output Format
Each activity.json contains:
{
"scope": "project 43483623",
"date": "2026-03-24",
"events": [
{
"id": 123456,
"kind": "message_created",
"created_at": "2026-03-24T14:30:00Z",
"creator": { "id": 789, "name": "Jeremy" },
"title": "Weekly update",
"url": "https://3.basecampapi.com/..."
}
],
"metadata": { "complete": true, "count": 5 }
}Arguments
| Argument | Required | Description |
|---|---|---|
--project | One of project/person | Basecamp project ID |
--person | One of project/person | Person name or ID (passed to basecamp timeline --person) |
--since | Yes | Start date (YYYY-MM-DD) |
--until | No | End date (default: today) |
--reuse | No | Skip fetch if cache exists and is complete |
Cache Structure
~/.cache/recap/basecamp-project/
43483623/
2026-03-24/activity.json
2026-03-25/activity.jsonPrerequisites
basecampCLI installed and authenticated (basecamp auth login)jqfor JSON parsingcurlfor API calls
Failure Modes
| Symptom | Cause | Fix |
|---|---|---|
| "basecamp not authenticated" | Token expired | basecamp auth login |
| "account not configured" | Missing config | basecamp config setup |
| Rate limit | Too many API calls | Script retries automatically with backoff |
| Empty activity | No events in range | Expected — empty days are cached as complete |
| HTTP 404 | Wrong project ID | Verify project ID in Basecamp URL |
#!/usr/bin/env bash
#
# Basecamp activity fetcher — project or person scoped, day-cached.
#
# Uses the `basecamp` CLI (timeline command) for authentication and fetching.
# Caches activity per day at ~/.cache/recap/basecamp-project/{id}/{YYYY-MM-DD}/activity.json
# or ~/.cache/recap/basecamp-person/{slug}/{YYYY-MM-DD}/activity.json.
#
# Usage:
# ./basecamp-activity.sh --project PROJECT_ID --since DATE --until DATE [--reuse]
# ./basecamp-activity.sh --person SLUG --since DATE --until DATE [--reuse]
#
set -euo pipefail
# Activate mise for basecamp CLI
eval "$(mise hook-env 2>/dev/null)" || true
PROJECT_ID=""
PERSON=""
SINCE_DATE=""
UNTIL_DATE=""
REUSE=false
while [[ $# -gt 0 ]]; do
case $1 in
--project) PROJECT_ID="$2"; shift 2 ;;
--person) PERSON="$2"; shift 2 ;;
--since) SINCE_DATE="$2"; shift 2 ;;
--until) UNTIL_DATE="$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
if [[ -z "$PROJECT_ID" && -z "$PERSON" ]]; then
echo "Error: --project PROJECT_ID or --person SLUG is required" >&2
exit 1
fi
[[ -z "$UNTIL_DATE" ]] && UNTIL_DATE=$(date -u +%Y-%m-%d)
SINCE_DAY="${SINCE_DATE:0:10}"
UNTIL_DAY="${UNTIL_DATE:0:10}"
# Validate auth
if ! basecamp auth status --quiet >/dev/null 2>&1; then
echo "Error: basecamp not authenticated. Run: basecamp auth login" >&2
exit 1
fi
# Determine cache path and scope
if [[ -n "$PROJECT_ID" ]]; then
CACHE_BASE="$HOME/.cache/recap/basecamp-project/$PROJECT_ID"
SCOPE="project $PROJECT_ID"
else
CACHE_BASE="$HOME/.cache/recap/basecamp-person/$PERSON"
SCOPE="person $PERSON"
fi
echo "Fetching Basecamp activity for $SCOPE ($SINCE_DAY to $UNTIL_DAY)..." >&2
# Generate list of days in range
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 fetch entirely with --reuse)
if [[ "$REUSE" == "true" ]]; then
ALL_COMPLETE=true
for day in $(days_in_range "$SINCE_DAY" "$UNTIL_DAY"); do
CACHE_FILE="$CACHE_BASE/$day/activity.json"
if ! jq -e '.metadata.complete == true' "$CACHE_FILE" >/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
DAY_FILES=()
for day in $(days_in_range "$SINCE_DAY" "$UNTIL_DAY"); do
DAY_FILES+=("$CACHE_BASE/$day/activity.json")
done
TOTAL=$(jq -s '[.[].metadata.count] | add' "${DAY_FILES[@]}")
echo '{"status":"complete","cache_base":"'"$CACHE_BASE"'","total_events":'"$TOTAL"'}'
exit 0
fi
fi
# Fetch all activity events using the basecamp timeline CLI, then split by day.
ALL_EVENTS_FILE=$(mktemp)
FETCH_FAILED=false
if [[ -n "$PROJECT_ID" ]]; then
echo " Fetching project timeline..." >&2
basecamp timeline --in "$PROJECT_ID" --json --quiet --all 2>/dev/null > "$ALL_EVENTS_FILE" || {
echo " ERROR: timeline fetch failed for project $PROJECT_ID" >&2
FETCH_FAILED=true
}
else
echo " Fetching person timeline ($PERSON)..." >&2
basecamp timeline --person "$PERSON" --json --quiet --all 2>/dev/null > "$ALL_EVENTS_FILE" || {
echo " ERROR: timeline fetch failed for person $PERSON" >&2
FETCH_FAILED=true
}
fi
if [[ "$FETCH_FAILED" == "true" ]]; then
rm -f "$ALL_EVENTS_FILE"
echo '{"status":"error","message":"timeline fetch failed"}' >&2
exit 1
fi
# Filter to window
SINCE_ISO="${SINCE_DAY}T00:00:00Z"
UNTIL_ISO="${UNTIL_DAY}T23:59:59Z"
jq --arg since "$SINCE_ISO" --arg until "$UNTIL_ISO" '
[.[] | select(.created_at >= $since and .created_at <= $until)] |
sort_by(.created_at) | reverse
' "$ALL_EVENTS_FILE" > "${ALL_EVENTS_FILE}.filtered"
TOTAL=$(jq 'length' "${ALL_EVENTS_FILE}.filtered")
echo " $TOTAL events in window" >&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
echo " $day: reusing cache" >&2
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)
DAY_START="${day}T00:00:00Z"
DAY_END="${NEXT_DAY}T00:00:00Z"
jq --arg start "$DAY_START" --arg end "$DAY_END" --arg scope "$SCOPE" --arg day "$day" '
[.[] | select(.created_at >= $start and .created_at < $end)] |
{
scope: $scope,
date: $day,
events: .,
metadata: { complete: true, count: (. | length) }
}
' "${ALL_EVENTS_FILE}.filtered" > "$CACHE_FILE"
COUNT=$(jq '.metadata.count' "$CACHE_FILE")
[[ "$COUNT" -gt 0 ]] && echo " $day: $COUNT events" >&2
done
rm -f "$ALL_EVENTS_FILE" "${ALL_EVENTS_FILE}.filtered"
echo '{"status":"complete","cache_base":"'"$CACHE_BASE"'","total_events":'"$TOTAL"'}'