# Cron Job: garmin-weekly

**Job ID:** 82532fd0a6ff
**Run Time:** 2026-06-07 19:18:04
**Schedule:** 0 18 * * 0

## Prompt

[IMPORTANT: The user has invoked the "google-workspace" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]

---
name: google-workspace
description: "Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python."
version: 1.1.0
author: Nous Research
license: MIT
platforms: [linux, macos, windows]
required_credential_files:
  - path: google_token.json
    description: Google OAuth2 token (created by setup script)
  - path: google_client_secret.json
    description: Google OAuth2 client credentials (downloaded from Google Cloud Console)
metadata:
  hermes:
    tags: [Google, Gmail, Calendar, Drive, Sheets, Docs, Contacts, Email, OAuth]
    homepage: https://github.com/NousResearch/hermes-agent
    related_skills: [himalaya]
---

# Google Workspace

Gmail, Calendar, Drive, Contacts, Sheets, and Docs — through Hermes-managed OAuth and a thin CLI wrapper. When `gws` is installed, the skill uses it as the execution backend for broader Google Workspace coverage; otherwise it falls back to the bundled Python client implementation.

## References

- `references/gmail-search-syntax.md` — Gmail search operators (is:unread, from:, newer_than:, etc.)
- `references/google-calendar-event-audit.md` — Google Calendar full-year loose-event scans, placeholder detection, enrichment workflow, and Calendar API pitfalls.

## Scripts

- `scripts/setup.py` — OAuth2 setup (run once to authorize)
- `scripts/google_api.py` — compatibility wrapper CLI. It prefers `gws` for operations when available, while preserving Hermes' existing JSON output contract.

## First-Time Setup

The setup is fully non-interactive — you drive it step by step so it works
on CLI, Telegram, Discord, or any platform.

Define a shorthand first:

```bash
GSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py"
```

### Step 0: Check if already set up

```bash
$GSETUP --check
```

If it prints `AUTHENTICATED`, skip to Usage — setup is already done.

### Step 1: Triage — ask the user what they need

Before starting OAuth setup, ask the user TWO questions:

**Question 1: "What Google services do you need? Just email, or also
Calendar/Drive/Sheets/Docs?"**

- **Email only** → They don't need this skill at all. Use the `himalaya` skill
  instead — it works with a Gmail App Password (Settings → Security → App
  Passwords) and takes 2 minutes to set up. No Google Cloud project needed.
  Load the himalaya skill and follow its setup instructions.

- **Email + Calendar** → Continue with this skill, but use
  `--services email,calendar` during auth so the consent screen only asks for
  the scopes they actually need.

- **Calendar/Drive/Sheets/Docs only** → Continue with this skill and use a
  narrower `--services` set like `calendar,drive,sheets,docs`.

- **Full Workspace access** → Continue with this skill and use the default
  `all` service set.

**Question 2: "Does your Google account use Advanced Protection (hardware
security keys required to sign in)? If you're not sure, you probably don't
— it's something you would have explicitly enrolled in."**

- **No / Not sure** → Normal setup. Continue below.
- **Yes** → Their Workspace admin must add the OAuth client ID to the org's
  allowed apps list before Step 4 will work. Let them know upfront.

### Step 2: Create OAuth credentials (one-time, ~5 minutes)

Tell the user:

> You need a Google Cloud OAuth client. This is a one-time setup:
>
> 1. Create or select a project:
>    https://console.cloud.google.com/projectselector2/home/dashboard
> 2. Enable the required APIs from the API Library:
>    https://console.cloud.google.com/apis/library
>    Enable: Gmail API, Google Calendar API, Google Drive API,
>    Google Sheets API, Google Docs API, People API
> 3. Create the OAuth client here:
>    https://console.cloud.google.com/apis/credentials
>    Credentials → Create Credentials → OAuth 2.0 Client ID
> 4. Application type: "Desktop app" → Create
> 5. If the app is still in Testing, add the user's Google account as a test user here:
>    https://console.cloud.google.com/auth/audience
>    Audience → Test users → Add users
> 6. Download the JSON file and tell me the file path
>
> Important Hermes CLI note: if the file path starts with `/`, do NOT send only the bare path as its own message in the CLI, because it can be mistaken for a slash command. Send it in a sentence instead, like:
> `The JSON file path is: /home/user/Downloads/client_secret_....json`

Once they provide the path:

```bash
$GSETUP --client-secret /path/to/client_secret.json
```

If they paste the raw client ID / client secret values instead of a file path,
write a valid Desktop OAuth JSON file for them yourself, save it somewhere
explicit (for example `~/Downloads/hermes-google-client-secret.json`), then run
`--client-secret` against that file.

### Step 3: Get authorization URL

Use the service set chosen in Step 1. In newer versions, examples are:

```bash
$GSETUP --auth-url --services email,calendar --format json
$GSETUP --auth-url --services calendar,drive,sheets,docs --format json
$GSETUP --auth-url --services all --format json
```

Compatibility note: some installed Hermes skill versions only accept `--auth-url` with no `--services` or `--format` arguments. If `setup.py --help` does not show those flags or `--auth-url --services ... --format json` fails with "unrecognized arguments", run:

```bash
$GSETUP --auth-url
```

That prints the authorization URL directly and uses the bundled default full Workspace scope set.

In newer versions, this returns JSON with an `auth_url` field and also saves the exact URL to
`~/.hermes/google_oauth_last_url.txt`. In compatibility mode, copy the printed URL from stdout.

Agent rules for this step:
- Extract the `auth_url` field or copied stdout URL and send that exact URL to the user as a single line.
- Tell the user that the browser will likely fail on `http://localhost:1` after approval, and that this is expected.
- Tell them to copy the ENTIRE redirected URL from the browser address bar.
- If the user gets `Error 403: access_denied`, send them directly to `https://console.cloud.google.com/auth/audience` to add themselves as a test user.

### Step 4: Exchange the code

The user will paste back either a URL like `http://localhost:1/?code=4/0A...&scope=...`
or just the code string. Either works. The `--auth-url` step stores a temporary
pending OAuth session locally so `--auth-code` can complete the PKCE exchange
later, even on headless systems:

```bash
$GSETUP --auth-code "THE_URL_OR_CODE_THE_USER_PASTED" --format json
```

If `--auth-code` fails because the code expired, was already used, or came from
an older browser tab, it now returns a fresh `fresh_auth_url`. In that case,
immediately send the new URL to the user and have them retry with the newest
browser redirect only.

### Step 5: Verify

```bash
$GSETUP --check
```

Should print `AUTHENTICATED`. Setup is complete — token refreshes automatically from now on.

### Notes

- Token is stored at `~/.hermes/google_token.json` and auto-refreshes.
- Pending OAuth session state/verifier are stored temporarily at `~/.hermes/google_oauth_pending.json` until exchange completes.
- If `gws` is installed, `google_api.py` points it at the same `~/.hermes/google_token.json` credentials file. Users do not need to run a separate `gws auth login` flow.
- To revoke: `$GSETUP --revoke`

## Usage

All commands go through the API script. Set `GAPI` as a shorthand:

```bash
GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"
```

### Gmail

```bash
# Search (returns JSON array with id, from, subject, date, snippet)
$GAPI gmail search "is:unread" --max 10
$GAPI gmail search "from:boss@company.com newer_than:1d"
$GAPI gmail search "has:attachment filename:pdf newer_than:7d"

# Read full message (returns JSON with body text)
$GAPI gmail get MESSAGE_ID

# Send
$GAPI gmail send --to user@example.com --subject "Hello" --body "Message text"
$GAPI gmail send --to user@example.com --subject "Report" --body "<h1>Q4</h1><p>Details...</p>" --html
$GAPI gmail send --to user@example.com --subject "Hello" --from '"Research Agent" <user@example.com>' --body "Message text"

# Reply (automatically threads and sets In-Reply-To)
$GAPI gmail reply MESSAGE_ID --body "Thanks, that works for me."
$GAPI gmail reply MESSAGE_ID --from '"Support Bot" <user@example.com>' --body "Thanks"

# Labels
$GAPI gmail labels
$GAPI gmail modify MESSAGE_ID --add-labels LABEL_ID
$GAPI gmail modify MESSAGE_ID --remove-labels UNREAD
```

### Calendar

```bash
# List events (defaults to next 7 days)
$GAPI calendar list
$GAPI calendar list --start 2026-03-01T00:00:00Z --end 2026-03-07T23:59:59Z

# Create event (ISO 8601 with timezone required)
$GAPI calendar create --summary "Team Standup" --start 2026-03-01T10:00:00-06:00 --end 2026-03-01T10:30:00-06:00
$GAPI calendar create --summary "Lunch" --start 2026-03-01T12:00:00Z --end 2026-03-01T13:00:00Z --location "Cafe"
$GAPI calendar create --summary "Review" --start 2026-03-01T14:00:00Z --end 2026-03-01T15:00:00Z --attendees "alice@co.com,bob@co.com"

# Delete event
$GAPI calendar delete EVENT_ID
```

### Drive

```bash
# Search existing files
$GAPI drive search "quarterly report" --max 10
$GAPI drive search "mimeType='application/pdf'" --raw-query --max 5

# Get metadata for a single file
$GAPI drive get FILE_ID

# Upload a local file (auto-detects MIME type)
$GAPI drive upload /path/to/report.pdf
$GAPI drive upload /path/to/image.png --name "Logo.png" --parent FOLDER_ID

# Download (binary files download as-is; Google-native files export to a
# sensible default — Docs→pdf, Sheets→csv, Slides→pdf, Drawings→png)
$GAPI drive download FILE_ID
$GAPI drive download DOC_ID --output ~/doc.pdf
$GAPI drive download DOC_ID --export-mime text/plain --output ~/doc.txt

# Create a folder
$GAPI drive create-folder "Reports"
$GAPI drive create-folder "Q4" --parent FOLDER_ID

# Share
$GAPI drive share FILE_ID --email alice@example.com --role reader
$GAPI drive share FILE_ID --email alice@example.com --role writer --notify
$GAPI drive share FILE_ID --type anyone --role reader        # anyone with link
$GAPI drive share FILE_ID --type domain --domain example.com --role reader

# Delete — defaults to trash (reversible). Use --permanent to skip the trash.
$GAPI drive delete FILE_ID
$GAPI drive delete FILE_ID --permanent
```

### Contacts

```bash
$GAPI contacts list --max 20
```

### Sheets

```bash
# Create a new spreadsheet
$GAPI sheets create --title "Q4 Budget"
$GAPI sheets create --title "Inventory" --sheet-name "Stock"

# Read
$GAPI sheets get SHEET_ID "Sheet1!A1:D10"

# Write
$GAPI sheets update SHEET_ID "Sheet1!A1:B2" --values '[["Name","Score"],["Alice","95"]]'

# Append rows
$GAPI sheets append SHEET_ID "Sheet1!A:C" --values '[["new","row","data"]]'
```

### Docs

```bash
# Read
$GAPI docs get DOC_ID

# Create a new Doc (optionally seeded with body text)
$GAPI docs create --title "Meeting Notes"
$GAPI docs create --title "Draft" --body "First paragraph..."

# Append text to the end of an existing Doc
$GAPI docs append DOC_ID --text "Additional content to append"
```

## Output Format

All commands return JSON. Parse with `jq` or read directly. Key fields:

- **Gmail search**: `[{id, threadId, from, to, subject, date, snippet, labels}]`
- **Gmail get**: `{id, threadId, from, to, subject, date, labels, body}`
- **Gmail send/reply**: `{status: "sent", id, threadId}`
- **Calendar list**: `[{id, summary, start, end, location, description, htmlLink}]`
- **Calendar create**: `{status: "created", id, summary, htmlLink}`
- **Drive search**: `[{id, name, mimeType, modifiedTime, webViewLink}]`
- **Drive get**: `{id, name, mimeType, modifiedTime, size, webViewLink, parents, owners}`
- **Drive upload**: `{status: "uploaded", id, name, mimeType, webViewLink}`
- **Drive download**: `{status: "downloaded", id, name, path, mimeType}`
- **Drive create-folder**: `{status: "created", id, name, webViewLink}`
- **Drive share**: `{status: "shared", permissionId, fileId, role, type}`
- **Drive delete**: `{status: "trashed" | "deleted", fileId, permanent}`
- **Contacts list**: `[{name, emails: [...], phones: [...]}]`
- **Sheets get**: `[[cell, cell, ...], ...]`
- **Sheets create**: `{status: "created", spreadsheetId, title, spreadsheetUrl}`
- **Docs create**: `{status: "created", documentId, title, url}`
- **Docs append**: `{status: "appended", documentId, inserted_at, characters}`

## Rules

1. **Never send email, create/delete calendar events, delete Drive files, share files, or modify Docs/Sheets without confirming with the user first.** Show what will be done (recipients, file IDs, content, share role) and ask for approval. For `drive delete`, prefer the default trash (reversible) over `--permanent`.

2. **The `gmail send` command does NOT support file attachments.** To send an email with a file attached, write a Python script using the Google API directly (see `references/gmail-attachment-workaround.md`). The approach: build a `MIMEMultipart('mixed')` message, attach a `MIMEBase` with `encoders.encode_base64()`, then use `service.users().messages().send()` with the raw base64-encoded payload.
2. **Check auth before first use** — run `setup.py --check`. If it fails, guide the user through setup.
3. **Use the Gmail search syntax reference** for complex queries — load it with `skill_view("google-workspace", file_path="references/gmail-search-syntax.md")`.
4. **Calendar times must include timezone** — always use ISO 8601 with offset (e.g., `2026-03-01T10:00:00-06:00`) or UTC (`Z`).
5. **Respect rate limits** — avoid rapid-fire sequential API calls. Batch reads when possible.

## Troubleshooting

| Problem | Fix |
|---------|-----|
| `NOT_AUTHENTICATED` | Run setup Steps 2-5 above |
| `REFRESH_FAILED` | Token revoked or expired — redo Steps 3-5 |
| `HttpError 403: Insufficient Permission` | Missing API scope — `$GSETUP --revoke` then redo Steps 3-5 |
| `AUTHENTICATED (partial)` or "Token missing scopes" | New write capabilities (Drive write/delete, Docs create/edit) require re-authorization. `$GSETUP --revoke` then redo Steps 3-5 to grant the upgraded scopes. |
| `HttpError 403: Access Not Configured` | API not enabled — user needs to enable it in Google Cloud Console |
| `ModuleNotFoundError` — `googleapiclient` not found when running stand-alone scripts | The `google_api.py` script uses `#!/usr/bin/env python3` by default which may point to the system Python (PEP 668-managed). Fix: change shebang to a working venv, e.g. `#!/opt/hermes/.venv/bin/python3`, or invoke with `path/to/venv/bin/python google_api.py`. The skill's `scripts/google_api.py` already has this corrected when installed via Hermes — only applies when you copy the file elsewhere. |
| | Run `$GSETUP --install-deps` |
| Advanced Protection blocks auth | Workspace admin must allowlist the OAuth client ID |

## Revoking Access

```bash
$GSETUP --revoke
```

The user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]

## Script Output
The following data was collected by a pre-run script. Use it as context for your analysis.

```
# Garmin Weekly Briefing
**31 May – 07 Jun 2026**

---

## 🏃 Activiteiten
**15 activiteiten** – 197.1 km – 6h9m – 2685 kCal

| Activiteit | Aantal | Afstand | Tijd | Gem. HR |
|---|---|---|---|---|
| e_bike_fitness | 7 | 102.7 km | 3h4m | 94 bpm |
| virtual_ride | 6 | 91.8 km | 2h56m | 129 bpm |
| cycling | 2 | 2.6 km | 8m | 99 bpm |

- **Totale trainingsbelasting**: 633
- **Gemiddelde aerobe TE**: 1.2
- **Intensiteitsminuten**: 2 matig / 88 intensief

---

## 😴 Slaap
**8 nachten**

- **Gemiddelde score**: 84 (range 64–94)
- **Gemiddelde duur**: 7.3h (range 5.6–8.5h)
- **Deep sleep**: gem. 78min (52–129)
- **REM sleep**: gem. 98min (31–138)
- **HRV**: gem. 51ms (43–60)
- **Rust-HR**: gem. 47bpm (44–50)

| Dag | Score | Totaal | Deep | REM | HRV | rHR |
|---|---|---|---|---|---|---|
| Sun 05-31 | 86 | 8.5h | 67m | 115m | 54 | 48 |
| Mon 06-01 | 93 | 7.8h | 83m | 104m | 60 | 45 |
| Tue 06-02 | 94 | 7.2h | 129m | 101m | 56 | 44 |
| Wed 06-03 | 82 | 7.0h | 56m | 138m | 52 | 44 |
| Thu 06-04 | 92 | 7.6h | 103m | 101m | 52 | 45 |
| Fri 06-05 | 87 | 8.4h | 52m | 104m | 46 | 49 |
| Sat 06-06 | 75 | 6.5h | 59m | 89m | 46 | 49 |
| Sun 06-07 | 64 | 5.6h | 77m | 31m | 43 | 50 |

---

## 🔋 Herstel & Body Battery
**7 dagen**

- **Wake BB**: gem. 89 (range 55–100)
- **Dagelijkse range**: 32–89 (gem. laag–hoog)
- **Opladen**: gem. +55/nacht (range +47–+66)
- **Verbruik**: gem. -51/dag (range -14–-71)

| Dag | Wake | Hoog | Laag | Charge | Drain |
|---|---|---|---|---|---|
| Mon 06-01 | 100 | 100 | 40 | +53 | -64 |
| Tue 06-02 | 99 | 99 | 48 | +66 | -60 |
| Wed 06-03 | 98 | 98 | 34 | +53 | -64 |
| Thu 06-04 | 99 | 99 | 43 | +56 | -14 |
| Fri 06-05 | 94 | 95 | 26 | +57 | -71 |
| Sat 06-06 | 79 | 79 | 17 | +56 | -65 |
| Sun 06-07 | 55 | 56 | 13 | +47 | -19 |

---

## 😰 Stress
- **5074 metingen** deze week
- **Gemiddeld**: 18
- **Range**: -2–99
- **Verdeling**: rest 82% · laag 12% · medium 4% · hoog 2%

---

## ⚖️ Gewicht
_Geen nieuwe metingen deze week._

---

## 🎯 Fitheid
- **Fitness Age**: 38 (chronologisch 43, achievable 37)
- **VO₂max (cycle)**: 49

---

## 🏁 Race Predictions
- **5K**: 25:20
- **10K**: 55:31
- **Half Marathon**: 2:08:37
- **Marathon**: 4:54:07

---

*Automatisch gegenereerd op Sunday 07 June 2026 21:08*
```

The script /opt/data/scripts/garmin-weekly-briefing.py printed markdown briefing output. Save that output to /opt/data/cron/output/garmin-weekly/$(date -u +%Y-%m-%d).md. Then read the file and send an email to tomstynen@gmail.com with subject "Garmin Weekly Briefing — [week range from the first heading line]" containing the markdown content as the email body. Use the google-workspace skill to send the email.

## Response

The Garmin Weekly Briefing for 31 May – 07 Jun 2026 has been successfully generated, saved to `/opt/data/cron/output/garmin-weekly/2026-06-07.md`, and sent to `tomstynen@gmail.com`.

**Email Status:**
- **Recipient:** tomstynen@gmail.com
- **Subject:** Garmin Weekly Briefing — 31 May – 07 Jun 2026
- **Result:** Sent (ID: 19ea38551338f936)
