---
name: productivity-toolkit
description: "Integrate with external productivity SaaS: Google Workspace, Notion, Airtable, Teams, PDF/OCR, PowerPoint, maps, daily briefings, job-search templates, and (via `petdex`) mascot setup."
version: 2.1.0
author: Hermes Agent
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [Productivity, Google-Workspace, Notion, Airtable, PDF, OCR, PowerPoint, Maps, Teams, Briefing, job-search, mascot]
    related_skills: [himalaya, apple-ecosystem, job-search-toolkit, productivity-toolkit]
---

# Productivity Toolkit

Class-level skill for integrating with external productivity SaaS tools via API and CLI. Covers office suites, knowledge bases, document processing, mapping, and automated digests. The shared pattern is: authenticate → read/write → handle pagination/errors → clean up.

> **Load this skill when the user mentions any of:** Gmail, Calendar, Drive, Docs, Sheets, Notion, Airtable, Teams meetings, PDF editing, OCR, PowerPoint, maps/geocoding, daily digests, or morning briefings.

---

## Cross-Tool Patterns

These patterns apply to most tools in this skill:

### Authentication

1. **OAuth2 flow** (Google Workspace, Teams): Run setup script once, store token in `~/.hermes/.env` or a credential file. Tokens expire — refresh flows are handled automatically by the CLI/SDK.
2. **API key** (Notion, Airtable, maps): Generate from the service's developer console, store in `${HERMES_HOME:-~/.hermes}/.env` as `SERVICE_API_KEY=...`
3. **Token scopes:** Request minimum scopes. For Google, that's `gmail.readonly`, `calendar`, `drive.file`, `sheets`, `docs`. For Airtable, add each base to the token's Access list or you get 403.

### Rate Limiting

- **Google Workspace:** 100 req/sec per user (burst), 1,000/day for Gmail
- **Notion:** ~3 req/sec sustained
- **Airtable:** 5 req/sec per base (free), 50 req/sec (enterprise)
- **Nominatim (maps):** 1 req/sec max — add `sleep 1` between requests

Handle 429 with exponential backoff:

```python
import time, random
for attempt in range(5):
    try:
        response = make_request()
        break
    except RateLimitError:
        time.sleep((2 ** attempt) + random.random())
```

### Pagination

| Service | Pagination style | Key parameter |
|---------|-----------------|---------------|
| Google | `nextPageToken` | `pageToken` in query string |
| Notion | `next_cursor` | `start_cursor` in body |
| Airtable | `offset` | `offset` in query string |
| Nominatim | None (hard limit ~40 results) | N/A |

Pattern for all:
```python
items = []
page_token = None
while True:
    resp = fetch(page_token=page_token)
    items.extend(resp["results"])
    page_token = resp.get("next_page_token") or resp.get("next_cursor") or resp.get("offset")
    if not page_token:
        break
```

### Error Handling

- `401 Unauthorized` → token expired or missing scope. Refresh or re-auth.
- `403 Forbidden` → insufficient permissions. Check token scopes or sharing settings.
- `404 Not Found` → resource doesn't exist or was deleted.
- `429 Too Many Requests` → backoff and retry (see above).
- `500/502/503` → transient server error. Retry with backoff.

---

## 1. Google Workspace (Gmail, Calendar, Drive, Docs, Sheets)

### Setup

Requires: `google_token.json` (OAuth2 token) and `google_client_secret.json` (OAuth2 client credentials).

```bash
# Run the bundled setup script (one-time)
python3 ~/.hermes/skills/productivity/productivity-toolkit/scripts/google-workspace-setup.py

# The script will:
# 1. Open a browser for OAuth consent
# 2. Save google_token.json in ~/.hermes/
# 3. Store client secret path in ~/.hermes/.env
```

Alternatively, use `gws` CLI if installed:

```bash
pip install gws-cli

# Read unread emails (last 24h)
gws gmail list --unread --since "24h ago" --format table

# Search with operators
gws gmail search "from:user@example.com newer_than:7d is:unread" --max 20
```

### Gmail

```python
from scripts.google_api import GmailClient
gmail = GmailClient()

# List unread
for msg in gmail.search("is:unread", max_results=20):
    print(f"{msg['from']}: {msg['subject']}")

# Search syntax reference in `references/google-workspace-gmail-search-syntax.md`
```

**Search operators:** `from:`, `to:`, `subject:`, `label:`, `is:unread/read/important/starred`, `has:attachment`, `filename:pdf`, `newer_than:7d`, `older_than:1y`

### Calendar

```python
gcal = GoogleCalendarClient()
events = gcal.list_events(time_min="2026-06-19T00:00:00Z", max_results=10)
for e in events:
    print(f"{e['start']}: {e['summary']} @ {e.get('location', 'no location')}")
```

### Drive

```python
drive = GoogleDriveClient()
files = drive.search("name contains 'budget' and mimeType = 'application/vnd.google-apps.spreadsheet'")
for f in files:
    print(f"{f['name']} ({f['id']})")
    # Download as CSV
    drive.export_as_csv(f['id'], f"/tmp/{f['name']}.csv")
```

### Sheets

```python
sheets = GoogleSheetsClient()

# Read range
values = sheets.get_values("SPREADSHEET_ID", "Sheet1!A1:D10")

# Append row
sheets.append_row("SPREADSHEET_ID", "Sheet1", ["2026-06-19", "Sale", "1200", "USD"])

# Update cell
sheets.update_cell("SPREADSHEET_ID", "Sheet1!B5", "Updated value")
```

### Docs

```python
docs = GoogleDocsClient()

# Create doc
doc = docs.create("Meeting Notes - 2026-06-19")

# Insert text
docs.insert_text(doc['id'], "Body text here\n", index=1)

# Read doc
content = docs.get_text(doc['id'])
```

---

## 2. Notion

Two paths: `ntn` CLI (macOS/Linux, shorter syntax) or HTTP + `curl` (fallback, works everywhere).

### Setup

1. Create integration at https://notion.so/my-integrations
2. Copy the API key (`ntn_...` or `secret_...`)
3. Store in `~/.hermes/.env`: `NOTION_API_KEY=ntn_...`
4. Share each page/database with the integration (click ⋮ → Connections → your integration)

### ntn CLI (preferred)

```bash
ntn page create "New Project Brief" --database 0a1b2c3d4e5f
ntn database query 0a1b2c3d4e5f --filter '{"property":"Status","select":{"equals":"Done"}}'
ntn block append 0a1b2c3d4e5f --type paragraph --text "Updated status: shipped"
```

### curl Fallback

```bash
# List databases
curl "https://api.notion.com/v1/databases" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2022-06-28"

# Query a database
curl "https://api.notion.com/v1/databases/DB_ID/query" \
  -X POST \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d '{"filter":{"property":"Status","select":{"equals":"In Progress"}}}'
```

### Page Creation

```bash
curl "https://api.notion.com/v1/pages" -X POST \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2022-06-28" \
  -d '{
    "parent": {"database_id": "DB_ID"},
    "properties": {
      "Name": {"title": [{"text": {"content": "New task"}}]},
      "Status": {"select": {"name": "To Do"}}
    }
  }'
```

### Block Types Reference

See `references/notion-block-types.md` for all Notion block type JSON structures (paragraph, heading, bulleted_list_item, code, image, etc.).

---

## 3. Airtable

REST API via `curl`. No SDK, no OAuth flow — just `curl` and a personal access token.

### Setup

1. Create PAT at https://airtable.com/create/tokens (starts with `pat...`)
2. Scopes needed: `data.records:read`, `data.records:write`, `schema.bases:read`
3. **Critical:** Add each base to the token's Access list. PATs are per-base.
4. Store in `~/.hermes/.env`: `AIRTABLE_API_KEY=pat...`

### Base Schema Discovery

```bash
# List bases
curl "https://api.airtable.com/v0/meta/bases" \
  -H "Authorization: Bearer $AIRTABLE_API_KEY"

# List tables in a base
curl "https://api.airtable.com/v0/meta/bases/BASE_ID/tables" \
  -H "Authorization: Bearer $AIRTABLE_API_KEY"
```

### Records CRUD

```bash
# List records (paginated)
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME?maxRecords=100" \
  -H "Authorization: Bearer $AIRTABLE_API_KEY"

# Create a record
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME" -X POST \
  -H "Authorization: Bearer $AIRTABLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fields": {"Name": "New task", "Status": "In Progress"}, "typecast": true}'

# Update a record
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME/RECORD_ID" -X PATCH \
  -H "Authorization: Bearer $AIRTABLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fields": {"Status": "Done"}}'

# Delete a record
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME/RECORD_ID" -X DELETE \
  -H "Authorization: Bearer $AIRTABLE_API_KEY"
```

### Filtering

```bash
# Formula filter
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME?filterByFormula=Status%3D%27Done%27"

# Sort by date descending
curl "https://api.airtable.com/v0/BASE_ID/TABLE_NAME?sort%5B0%5D%5Bfield%5D=Created&sort%5B0%5D%5Bdirection%5D=desc"
```

---

## 4. Teams Meeting Pipeline

Microsoft Teams meeting summaries via the `hermes teams-pipeline` CLI.

### Setup

Requires: `MSGRAPH_TENANT_ID`, `MSGRAPH_CLIENT_ID`, `MSGRAPH_CLIENT_SECRET` in environment.

```bash
# Register an Azure AD app:
# 1. https://portal.azure.com > App registrations > New registration
# 2. Grant Microsoft Graph permissions: OnlineMeetings.Read.All, Chat.Read.All
# 3. Create client secret
# 4. Store credentials in ~/.hermes/.env
```

### CLI Commands

```bash
# Summarize a specific meeting
hermes teams-pipeline summarize --meeting-id "MEETING_ID"

# List recent meetings
hermes teams-pipeline list --days 7

# Check pipeline status
hermes teams-pipeline status

# Replay a failed job
hermes teams-pipeline replay --job-id "JOB_ID"

# Inspect stored meeting
hermes teams-pipeline inspect --meeting-id "MEETING_ID"
```

### Output Format

```json
{
  "meeting_id": "...",
  "title": "Weekly Standup",
  "participants": ["Alice", "Bob"],
  "transcript_url": "https://...",
  "summary": "Discussion focused on...",
  "action_items": [
    {"owner": "Alice", "task": "Fix auth middleware", "due": "2026-06-21"}
  ],
  "recording_url": "https://..."
}
```

---

## 5. Document Processing

### 5a. PDF Editing (nano-pdf)

```bash
pip install nano-pdf

# Edit text in a PDF (NL prompts)
nano-pdf edit contract.pdf "change all instances of 'Company A' to 'Acme Corp'"

# Fix a typo
nano-pdf edit report.pdf "fix the typo 'teh' to 'the' on page 5"

# Update titles
nano-pdf edit doc.pdf "set the title to 'Q2 Financial Report'"
```

### 5b. OCR and Document Extraction

Two backends: `pymupdf` (fast, embedded text) and `marker-pdf` (layout-aware, complex docs).

```bash
pip install pymupdf marker-pdf
```

**Embedded text extraction (pymupdf):**

```python
import fitz  # PyMuPDF
doc = fitz.open("document.pdf")
for page in doc:
    text = page.get_text()
    print(f"Page {page.number}: {text[:500]}...")
```

**Complex layout extraction (marker):**

```python
from marker.converters.pdf import PdfConverter
converter = PdfConverter()
result = converter("complex_layout.pdf")
# result.text — clean structured text
# result.images — extracted images
# result.tables — detected tables
```

Use scripts in `scripts/ocr-and-documents-extract_pymupdf.py` and `scripts/ocr-and-documents-extract_marker.py` for batch extraction.

### 5c. PowerPoint (.pptx)

```python
from pptx import Presentation

# Create a new deck
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])  # Title and Content
title = slide.shapes.title
title.text = "Q2 Results"

# Add content
content = slide.placeholders[1]
content.text = "- Revenue up 15%\n- User growth 23%"

# Save
prs.save("presentation.pptx")
```

Use `scripts/powerpoint-add_slide.py` for programmatic slide generation.

---

## 6. Maps and Location Intelligence

Geocoding, POIs, routing, and timezones via OpenStreetMap/Nominatim, Overpass, and OSRM. Python stdlib only, no API key required.

### Quick Start

```python
# maps_client.py — see scripts/maps-maps_client.py
from maps_client import MapsClient
client = MapsClient()

# Geocode
lat, lon = client.geocode("1600 Amphitheatre Parkway, Mountain View, CA")

# Reverse geocode
address = client.reverse_geocode(37.422, -122.084)

# Find nearby POIs
restaurants = client.nearby(lat, lon, category="restaurant", radius=500)

# Calculate route
distance, duration = client.route((37.422, -122.084), (37.774, -122.419))

# Timezone
tz = client.timezone(lat, lon)
```

### Data Sources

| Source | Endpoint | Use for |
|--------|----------|---------|
| Nominatim | `nominatim.openstreetmap.org` | Geocoding, reverse geocoding |
| Overpass | `overpass-api.de` | POI queries, complex spatial filters |
| OSRM | `router.project-osrm.org` | Driving/walking routes |
| TimeAPI | `timeapi.io` | Timezone lookup |

**Rate limit:** Nominatim requires ≤1 req/sec. Always add `sleep 1` between requests. Cache results locally.

---

## 7. Automated Daily Briefings

Combine calendar, news, weather, and custom content into a single message delivered via cron.

### Architecture

```
Cron trigger → fetch sources (calendar, news, weather) →
compile message → deliver to channel (Telegram, Discord, email, SMS)
```

### Quick Setup

```bash
# Use the daily briefing template
cp templates/daily-briefing-daily-briefing.py ~/briefing/
chmod +x ~/briefing/daily-briefing.py
```

### Cron Job

```cron
# Every weekday at 8 AM
0 8 * * 1-5 /home/user/briefing/daily-briefing.py >> /tmp/briefing.log 2>&1
```

### Sources You Can Mix

| Source | How to fetch |
|--------|-------------|
| Calendar | Google Calendar API or Apple CalDAV |
| News | RSS feeds (feedparser) or news API |
| Weather | OpenWeatherMap or wttr.in |
| Compliments | Rotating pool in a local file |
| Tasks | Todoist, Notion, or local todo list |
| Stock prices | Yahoo Finance API (no auth, no key) |

### Stock Prices via Yahoo Finance

Fetch live prices with Python stdlib only. No API key, no external dependencies.

```python
import urllib.request, json

def fetch_price(ticker: str) -> float | None:
    url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}"
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode())
            result = data.get("chart", {}).get("result", [{}])[0]
            price = result.get("meta", {}).get("regularMarketPrice")
            if price is None:
                price = result.get("meta", {}).get("previousClose")
            return float(price) if price else None
    except Exception:
        return None
```

**Pitfalls:**
- Yahoo Finance blocks requests without a `User-Agent` header. Always include one.
- `regularMarketPrice` is `None` when markets are closed; fall back to `previousClose`.
- Rate limits are unadvertised; keep requests modest (≤10 tickers is fine).

### Delivery Channels

| Channel | Method |
|---------|--------|
| Telegram | Bot API (`send_message`) |
| Discord | Webhook |
| Email | Gmail SMTP, Apple Mail SMTP, or Himalaya CLI |
| SMS | Twilio or carrier email gateways |

For Apple CalDAV setup and Gmail SMTP configuration, see `references/daily-briefing-apple-caldav.md` and `references/daily-briefing-gmail-smtp-setup.md`.

### Templates

- `templates/daily-briefing-daily-briefing.py` — full briefing script template
- `templates/daily-briefing-gmail-smtp-delivery.py` — SMTP delivery module
- `templates/daily-briefing-robust-caldav-fetcher.py` — Apple CalDAV fetcher with multi-strategy fallback (expand=True, expand=False, client-side filter) and deduplication

---

## 8. Job Search Toolkit

### 8a. Research Before Writing

Never write a generic email. Always research the target first:

1. **Company scope:** Search for company name + country/sector. Find size, founding year, revenue, key products, and culture.
2. **Subsidiaries/ecosystem:** Many large groups operate as clusters. Identify which subsidiary fits the user's stack.
3. **Contact background:** Search the recipient's name + company + LinkedIn. Note their role, recent posts, and shared connections.
4. **Recent news:** Search for press releases, awards, or LinkedIn activity from the last 12 months.
5. **Tech stack alignment:** Cross-reference the company's tech with the user's skills. Mention specific overlap.

Store research findings in a scratchpad or reference file. Cite 2-3 specific facts in the email body to signal genuine interest.

### 8b. Tone Calibration

Match tone to the user's preference. Ask or infer:
- **Formal:** Full sentences, no contractions, "I would like to," "I am writing to inquire..."
- **Casual:** Contractions okay, direct language, "Hi," "I'd love to," "Looking forward to hearing from you!"
- **In between:** Semi-professional, warm but respectful.

**Default:** Semi-casual for tech roles at startups/smaller companies; formal for banks, government, and EU institutions.

### 8c. Cold Outreach Structure

| Section | Purpose | Length |
|---------|---------|--------|
| **Subject** | Clear + personal connection | 6-10 words |
| **Greeting** | Use first name if known | 1 line |
| **Hook** | Name-drop mutual contact or shared context | 1-2 sentences |
| **Who You Are** | 2-3 sentences: role, stack, standout project | 3-4 lines |
| **Why Them** | 2-3 specific reasons this company/role fits | 3-4 lines |
| **Ask** | One clear CTA: call, coffee chat, meeting | 1-2 sentences |
| **Sign-off** | Full contact block: email, portfolio, LinkedIn | 3-4 lines |

### 8d. Pitfalls

- **Generic flattery:** Replace "I admire your company's commitment to excellence" with specific facts.
- **Wall of text:** Keep paragraphs to 3-4 lines max. Use whitespace.
- **Multiple asks:** One CTA per email.
- **Em-dash overuse:** Some users strongly dislike "—" as a sentence separator. Use commas, periods, colons, or restructure.
- **Forgetting portfolio link:** Tech candidates must include portfolio/GitHub in every outreach email.

### 8e. Templates

- `templates/job-search-cold-outreach-referral.md` — Starter for referral-based cold outreach
- `templates/job-search-application-letter.md` — Generic cover letter scaffold
- `templates/job-search-follow-up-interview.md` — Short thank-you / follow-up note

### 8f. References

- `references/job-search-cronos-group-research.md` — Example company research output showing depth, structure, and fact weaving
- `references/job-search-email-tone-examples.md` — Side-by-side formal vs. casual vs. semi-casual paragraphs

---

## 9. Petdex Mascots

Animated "pet" mascots for the Hermes CLI/TUI/desktop. Install, select, scale, and diagnose via `hermes pets`:

```bash
hermes pets list cat
hermes pets install <slug> --select
hermes pets show
hermes pets doctor
```

See `petdex` skill for full configuration and troubleshooting.

---

## Support Files

- `scripts/google-workspace-setup.py` — Google OAuth2 one-time setup
- `scripts/google-workspace-google_api.py` — Python API client wrapper
- `scripts/google-workspace-gws_bridge.py` — gws CLI bridge
- `scripts/ocr-and-documents-extract_pymupdf.py` — Batch PDF text extraction
- `scripts/ocr-and-documents-extract_marker.py` — Complex layout PDF extraction
- `scripts/powerpoint-add_slide.py` — Programmatic slide generation
- `scripts/powerpoint-clean.py` — Deck initialization helper
- `scripts/maps-maps_client.py` — Maps Python client (geocode, POIs, routing)
- `templates/daily-briefing-daily-briefing.py` — Daily briefing template
- `templates/daily-briefing-gmail-smtp-delivery.py` — SMTP delivery module
- `references/google-workspace-gmail-search-syntax.md` — Gmail search operators
- `references/notion-block-types.md` — Notion block type JSON reference
- `references/daily-briefing-apple-caldav.md` — Apple Calendar CalDAV setup
- `references/daily-briefing-gmail-smtp-setup.md` — Gmail SMTP configuration
- `references/daily-briefing-personalization-from-chat-data.md` — How to mine partner chat exports for themed compliments, date ideas, and gift ideas to personalize daily briefings
- `references/daily-briefing-rss-with-links.md` — RSS feed parsing with article links
- `references/daily-briefing-cronjob-patterns.md` — Cron patterns for scheduling
- `references/stock-price-patterns.md` — Yahoo Finance fetch + threshold alert state tracking

---

## 9. Petdex Mascots

Animated "pet" mascots for the Hermes CLI/TUI/desktop. Install, select, scale, and diagnose via `hermes pets`:

```bash
hermes pets list cat
hermes pets install <slug> --select
hermes pets show
hermes pets doctor
```

Full configuration and troubleshooting: `petdex` skill (archived under `.archive/productivity/petdex`; practical commands are now summarized here).

---

## Workflow Examples

### Morning Standup Digest

```
1. Fetch today's Calendar events (Google Calendar API)
2. Fetch assigned tasks from Notion database
3. Fetch unread emails mentioning your name (Gmail search)
4. Compile into a brief → send to Slack webhook
```

### Document Ingestion Pipeline

```
1. Receive PDF attachment via Gmail
2. Extract text with marker-pdf (scripts/ocr-and-documents-extract_marker.py)
3. Store structured text in Airtable
4. Create Notion page linking to the Airtable record
```

### Research Trip Planning

```
1. Search for venues with maps_client.nearby()
2. Save shortlisted venues to Airtable
3. Create Calendar events for visits
4. Export itinerary as PowerPoint (scripts/powerpoint-add_slide.py)
```
