Building a Weekly Activity Aggregator

AutomationProductivityPythonmacOS

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:

  1. Run: Type weekly in your terminal.
  2. Copy: Copy the resulting JSON block.
  3. 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."