Building a Weekly Activity Aggregator
The Problem: The "Blank Page" Syndrome
We've all been there: it's Sunday evening, you want to post a weekly update on LinkedIn to share your progress, but you can't remember exactly what you actually did. You know you were productive, you spent hours in Google Docs, pushed a dozen commits to GitHub, and navigated a maze of technical documentation, but the specifics are gone.
The solution is to stop relying on memory and start relying on data.
The Architecture: Local Pull → AI Summary
The goal is to create a system that knows everything you did, without compromising your security. Instead of giving an AI agent full remote access to your laptop, we use a read-only pull pattern.
The system consists of four independent "Puller" scripts that extract specific data into JSON snapshots, and one "Aggregator" that combines them into a single context block for an LLM.
The Stack
- OS: macOS
- Language: Python 3.9+
- Data Store: Local JSON files (
~/.hermes/weekly/) - Trigger: Zsh Alias (
weekly)
Phase 1: Environment Setup
First, we create the directory structure to keep our binaries and our data separate.
mkdir -p ~/.local/bin
mkdir -p ~/.hermes/weekly
Phase 2: The Pullers
1. Browser History (pull-browser.py)
Most professional work happens in the browser. This script reads the SQLite database used by Chrome, Brave, or Edge.
The Logic: Since the browser locks the history file while open, the script creates a temporary copy of the database, queries the last 7 days of visits, and extracts the most visited domains.
#!/usr/bin/env python3
import json, os, shutil, sqlite3, sys, tempfile
from collections import Counter
from datetime import datetime, timedelta, timezone
from urllib.parse import urlparse
CANDIDATES = [os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/History"), os.path.expanduser("~/Library/Application Support/Google/Chrome/Profile 1/History")]
NOW = datetime.now(timezone.utc)
WEEK_AGO = NOW - timedelta(days=7)
CHROME_EPOCH = datetime(1601, 1, 1, tzinfo=timezone.utc)
def main():
src = next((p for p in CANDIDATES if os.path.exists(p)), None)
if not src: sys.exit(1)
tmpdir = tempfile.mkdtemp()
tmp = os.path.join(tmpdir, "History")
shutil.copy2(src, tmp)
conn = sqlite3.connect(tmp)
cur = conn.cursor()
cutoff = int((WEEK_AGO - CHROME_EPOCH).total_seconds() * 1_000_000)
cur.execute("SELECT url, title, visit_count, last_visit_time FROM urls WHERE last_visit_time > ? ORDER BY last_visit_time DESC", (cutoff,))
rows = cur.fetchall()
conn.close()
shutil.rmtree(tmpdir)
domain_counts, titles = Counter(), []
for url, title, vc, lv in rows:
if "x.com" in url or "twitter.com" in url: continue
try: dom = urlparse(url).netloc or "(none)"
except: dom = "(unparseable)"
domain_counts[dom] += 1
if title: titles.append((datetime.fromtimestamp((lv / 1_000_000) + CHROME_EPOCH.timestamp(), tz=timezone.utc), title, url))
print("\n=== TOP DOMAINS (excluding X) ===")
for d, c in domain_counts.most_common(15): print(f" {c:4d} URLs {d}")
out_dir = os.path.expanduser("~/.hermes/weekly")
os.makedirs(out_dir, exist_ok=True)
with open(os.path.join(out_dir, "browser.json"), "w") as f:
json.dump({"source": "browser", "urls": [{"url": u, "title": t} for u, t, vc, lv in rows if "x.com" not in u and "twitter.com" not in u]}, f)
print("\nSaved to ~/.hermes/weekly/browser.json")
if __name__ == "__main__": main()
Key Output: ~/.hermes/weekly/browser.json
2. Google Workspace (pull-google.py)
Time-tracking is great, but content is better. This script uses the Google Drive and Calendar APIs to see what you actually touched.
The Logic: Using OAuth 2.0, it identifies:
- Drive: Files modified in the last 7 days (e.g., "Q3 Strategy Doc").
- Calendar: Meeting titles for the week (e.g., "Project Sync").
Setup: Requires a client_secret.json from the Google Cloud Console.
#!/usr/bin/env python3
import os, json, datetime
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/drive.readonly', 'https://www.googleapis.com/auth/calendar.readonly']
CRED_FILE = os.path.expanduser("~/.hermes/weekly/client_secret.json")
TOKEN_FILE = os.path.expanduser("~/.hermes/weekly/token.json")
OUT_FILE = os.path.expanduser("~/.hermes/weekly/google.json")
def get_creds():
creds = None
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(CRED_FILE, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_FILE, 'w') as token:
token.write(creds.to_json())
return creds
creds = get_creds()
drive = build('drive', 'v3', credentials=creds)
cal = build('calendar', 'v3', credentials=creds)
week_ago = (datetime.datetime.now() - datetime.timedelta(days=7)).isoformat() + 'Z'
now = datetime.datetime.utcnow().isoformat() + 'Z'
docs = drive.files().list(q=f"modifiedTime > '{week_ago}'", pageSize=10, fields="files(name)").execute().get('files', [])
events = cal.events().list(calendarId='primary', timeMin=week_ago, timeMax=now, singleEvents=True, orderBy='startTime').execute().get('items', [])
data = {
"docs": [d['name'] for d in docs],
"meetings": [f"{e.get('summary', 'No Title')} ({e['start'].get('dateTime', 'All Day')})" for e in events]
}
with open(OUT_FILE, 'w') as f:
json.dump(data, f)
print(f"Saved Google data to {OUT_FILE}")
Output: ~/.hermes/weekly/google.json
3. GitHub Activity (pull-github.py)
For the "Building in Public" aspect, we need a record of code contributions.
The Logic: It queries the GitHub /user/events endpoint using a Personal Access Token (PAT). It filters for PushEvent and WatchEvent to list the repos you've been active in.
#!/usr/bin/env python3
import os, json, urllib.request, urllib.error
from datetime import datetime, timedelta, timezone
TOKEN = os.environ.get("GITHUB_TOKEN")
if not TOKEN:
print("ERROR: GITHUB_TOKEN environment variable not set.")
exit(1)
def gh_get(url):
req = urllib.request.Request(f"https://api.github.com/{url}", headers={
"Authorization": f"token {TOKEN}",
"Accept": "application/vnd.github.v3+json"
})
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
print(f"GitHub API Error {e.code}: {e.read().decode()}")
return None
def main():
# Fetch username first to ensure we aren't hitting wrong paths
user = gh_get("user")
if not user: return
username = user["login"]
# Fetch events for the user
events = gh_get(f"users/{username}/events")
if events is None: return
activity = []
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
for ev in events:
created_at = datetime.strptime(ev["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
if created_at > week_ago:
type_ev = ev["type"]
repo = ev["repo"]["name"]
msg = f"[{created_at.strftime('%m-%d')}] {type_ev} on {repo}"
if type_ev == "PushEvent":
commits = ev["payload"].get("commits", [])
for c in commits:
msg += f"\n - {c['message']}"
activity.append(msg)
print(f"=== GITHUB ACTIVITY ({username}) ===")
for a in activity: print(a)
with open(os.path.expanduser("~/.hermes/weekly/github.json"), "w") as f:
json.dump(activity, f)
if __name__ == "__main__":
main()
Output: ~/.hermes/weekly/github.json
Phase 3: The Aggregator (aggregator.py)
Now we have four disparate JSON files. The Aggregator acts as the "translator," reading these files and normalizing them into a single JSON object that an LLM can understand.
It handles data type mismatches (e.g., converting tuples to strings) and trims the lists to the top 10-20 most relevant items to avoid hitting LLM token limits.
#!/usr/bin/env python3
"""
aggregator.py — gathers JSON outputs from pull-scripts and formats them for LinkedIn.
"""
import json
import os
from urllib.parse import urlparse
from collections import Counter
def load_json(path):
if not os.path.exists(path):
return None
try:
with open(path, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Error parsing {path}: {e}")
return None
def main():
wd = os.path.expanduser("~/.hermes/weekly")
browser = load_json(os.path.join(wd, "browser.json"))
google = load_json(os.path.join(wd, "google.json"))
github = load_json(os.path.join(wd, "github.json"))
aw_data = load_json(os.path.join(wd, "aw-data.json"))
summary = {
"browser_top_domains": [],
"google_docs": [],
"google_meetings": [],
"github_activity": [],
"aw_usage": []
}
# Browser Data Extraction
if browser and isinstance(browser, dict):
urls = browser.get("urls", [])
if urls:
domains = []
for item in urls:
# Handle both raw strings and dicts {"url": "...", ...}
u = item.get("url") if isinstance(item, dict) else item
if u:
domains.append(urlparse(u).netloc or u)
# Get top 10 most frequent domains
top_10 = [d for d, c in Counter(domains).most_common(10)]
summary["browser_top_domains"] = top_10
# Google Data Extraction
if google:
if isinstance(google, dict):
summary["google_docs"] = google.get("docs", [])
summary["google_meetings"] = google.get("meetings", [])
else:
summary["google_docs"] = google
# GitHub Data Extraction
if github:
if isinstance(github, list):
summary["github_activity"] = github[:20]
elif isinstance(github, dict):
summary["github_activity"] = github.get("events", github.get("activity", []))[:20]
# ActivityWatch Data Extraction
if aw_data:
if isinstance(aw_data, dict):
summary["aw_usage"] = aw_data.get("apps", aw_data.get("activity", []))[:20]
elif isinstance(aw_data, list):
summary["aw_usage"] = aw_data[:20]
print("\n=== DATA READY FOR LINKEDIN SUMMARY ===")
print(json.dumps(summary, indent=2))
print("\n--- Copy the JSON above and paste it to me ---")
if __name__ == "__main__":
main()
Output: A clean, structured JSON block printed to the terminal.
Phase 4: The Master Trigger
To make this a seamless part of the workflow, we wrap everything in a shell script and map it to a single word.
The Master Script: weekly-summary.sh
#!/bin/bash
echo "Collecting Weekly Data..."
~/.local/bin/pull-browser.py > /dev/null 2>&1
~/.local/bin/pull-google.py > /dev/null 2>&1
~/.local/bin/pull-github.py > /dev/null 2>&1
~/.local/bin/aw-weekly-pull.py > /dev/null 2>&1
echo "Generating Aggregated Summary..."
~/.local/bin/aggregator.py
The Zsh Alias
Add this to your ~/.zshrc:
alias weekly="~/.local/bin/weekly-summary.sh"
The Final Workflow
Once this is set up, your Sunday routine becomes:
- Run: Type
weeklyin your terminal. - Copy: Copy the resulting JSON block.
- Prompt: Paste it into your AI of choice with a prompt:
"Here is my activity data for the week. Based on this, draft 3 LinkedIn posts: one professional, one technical, and one short. Highlight my wins in [Your Industry] and keep the tone authentic."
By turning your digital exhaust into structured data, you remove the friction of "remembering" and replace it with the power of "reflecting."