Bot Process Control Manage the Gmail Commander bot daemon and scheduled digest via launchd. Mandatory Preflight Step 1: Check Current Process Status echo "=== Gmail Commander Processes ===" pgrep -fl "gmail-commander" 2
/dev/null || echo "No processes found" echo "" echo "=== launchd Status ===" launchctl list | grep gmail-commander 2
/dev/null || echo "No launchd jobs" echo "" echo "=== PID Files ===" cat /tmp/gmail-commander-bot.pid 2
/dev/null && echo " (bot)" || echo "No bot PID file" cat /tmp/gmail-digest.pid 2
/dev/null && echo " (digest)" || echo "No digest PID file" Two Services Service Type Trigger PID File Bot Daemon KeepAlive Always-on (grammY polling) /tmp/gmail-commander-bot.pid Digest StartInterval Every 6 hours (21600s) /tmp/gmail-digest.pid launchd Plist Templates Bot Daemon — com.terryli.gmail-commander-bot.plist
<! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"
< plist version = " 1.0 "
< dict
< key
Label </ key
< string
com.terryli.gmail-commander-bot </ string
< key
ProgramArguments </ key
< array
< string
{{HOME}}/own/amonic/bin/gmail-commander-bot </ string
</ array
< key
RunAtLoad </ key
< true /> < key
KeepAlive </ key
< dict
< key
NetworkState </ key
< true /> </ dict
< key
StandardOutPath </ key
< string
{{HOME}}/.local/state/launchd-logs/gmail-commander-bot/stdout.log </ string
< key
StandardErrorPath </ key
< string
{{HOME}}/.local/state/launchd-logs/gmail-commander-bot/stderr.log </ string
< key
EnvironmentVariables </ key
< dict
< key
PATH </ key
< string
{{HOME}}/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin </ string
</ dict
< key
ThrottleInterval </ key
< integer
10 </ integer
</ dict
</ plist
Scheduled Digest — com.terryli.gmail-commander-digest.plist
<! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"
< plist version = " 1.0 "
< dict
< key
Label </ key
< string
com.terryli.gmail-commander-digest </ string
< key
ProgramArguments </ key
< array
< string
{{HOME}}/own/amonic/bin/gmail-commander-digest </ string
</ array
< key
StartInterval </ key
< integer
21600 </ integer
< key
StandardOutPath </ key
< string
{{HOME}}/.local/state/launchd-logs/gmail-commander-digest/stdout.log </ string
< key
StandardErrorPath </ key
< string
{{HOME}}/.local/state/launchd-logs/gmail-commander-digest/stderr.log </ string
< key
EnvironmentVariables </ key
< dict
< key
PATH </ key
< string
{{HOME}}/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin </ string
</ dict
</ dict
</ plist
Quick Operations Start Bot launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist Stop Bot launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist Restart Bot launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist Force Kill (Emergency) pkill -f "gmail-commander.*bot.ts" rm -f /tmp/gmail-commander-bot.pid View Logs
Recent bot output (centralized launchd logs)
tail -50 ~/.local/state/launchd-logs/gmail-commander-bot/stderr.log
Recent digest output
tail -50 ~/.local/state/launchd-logs/gmail-commander-digest/stderr.log
Audit log (NDJSON, app-managed)
cat $PROJECT_DIR /logs/audit/ $( date +%Y-%m-%d ) .ndjson | jq .
OAuth token refresher log
- tail
- -20
- ~/.local/state/launchd-logs/gmail-oauth-refresher/stderr.log
- System Resources (Expected)
- Memory
-
- ~20-30 MB RSS (Bun runtime + grammY)
- CPU
-
- Negligible (idle polling, wakes on message)
- Network
-
- Minimal (single long-poll connection to Telegram API)
- Disk
-
- ~1 MB/day audit logs (14-day rotation)
- Telegram Commands
- Command
- Description
- /inbox
- Show recent inbox emails
- /search
- Search emails (Gmail query syntax)
- /read
- Read email by ID
- /compose
- Compose a new email
- /reply
- Reply to an email
- /abort
- Cancel current compose/reply action
- /drafts
- List draft emails
- /digest
- Run email digest now
- /status
- Bot status and stats
- /help
- Show all commands
- Note
- :
- /abort
- cancels any in-progress compose or reply session. Works at any step in the flow.
- OAuth Token Management
- Two-Layer Token Architecture
- Browser Auth (one-time, interactive)
- → Google issues: access_token (1h TTL) + refresh_token (7d TTL in Testing mode)
- → Saved to: ~/.claude/tools/gmail-tokens/
.json - Silent Refresh (automatic, no browser)
- → Uses refresh_token to get new access_token
- → Fails with invalid_grant when refresh_token itself expires
- Hourly Token Refresher (launchd)
- A compiled Swift binary runs hourly to proactively refresh the access token:
- File
- Path
- Source
- ~/.claude/automation/gmail-token-refresher/main.swift
- Binary
- ~/.claude/automation/gmail-token-refresher/gmail-oauth-token-hourly-refresher
- Plist
- ~/Library/LaunchAgents/com.terryli.gmail-oauth-token-hourly-refresher.plist
- Log
- $PROJECT_DIR/logs/token-refresher.log
- Why hourly
- Access tokens expire every 1 hour. Refreshing hourly keeps the token perpetually valid. Frequent refresh also increases the chance Google issues a new refresh_token , resetting its 7-day clock. Verify it's running : launchctl list | grep gmail-oauth-token tail -5 $PROJECT_DIR /logs/token-refresher.log Credentials source : GMAIL_OP_UUID item in 1Password Claude Automation vault (fields: client_id , client_secret ). Accessed via service account token — no biometric prompt required. Diagnosing invalid_grant invalid_grant means the refresh token itself expired (not just the access token):
Symptom in audit log:
cat $PROJECT_DIR /logs/audit/ $( date +%Y-%m-%d ) .ndjson | jq 'select(.event == "gmail.error")'
→ "Token expired, refreshing...\nError: invalid_grant\n"
Check token file age:
ls -la ~/.claude/tools/gmail-tokens/ < GMAIL_OP_UUID
.json Fix :
1. Delete expired token
rm ~/.claude/tools/gmail-tokens/ < GMAIL_OP_UUID
.json
2. Trigger browser re-auth (opens Google consent page)
source $PROJECT_DIR /.env.launchd $PLUGIN_DIR /scripts/gmail-cli/gmail list -n 1
3. Restart bot
- launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
- launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
- Root cause
- Google OAuth apps in Testing mode issue refresh tokens with 7-day TTL. Permanent fix: publish the Google Cloud OAuth app (Google Cloud Console → OAuth consent screen → Publish app). Diagnosing Stale PID Lock If the bot exits uncleanly, the PID file may block restart:
Symptom: launchctl shows bot loaded but PID is dead
kill -0 $( cat /tmp/gmail-commander-bot.pid ) 2
&1
→ "No such process"
Fix: restart via launchctl (acquireLock handles stale PIDs automatically)
launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist Post-Change Checklist YAML frontmatter valid (no colons in description) Trigger keywords current Path patterns use $HOME not hardcoded paths launchd plist templates match actual launcher scripts OAuth token refresher launchd service loaded and running