Gmail Commander daemon lifecycle - start, stop, restart, status, logs, launchd plist management. TRIGGERS - bot start, bot stop, bot restart, bot status, bot logs, launchd, daemon, process control, gmail-commander service.
From gmail-commandernpx claudepluginhub terrylica/cc-skills --plugin gmail-commanderThis skill is limited to using the following tools:
references/evolution-log.mdDesigns and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Manage the Gmail Commander bot daemon and scheduled digest via launchd.
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
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"
| 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 |
com.terryli.gmail-commander-bot.plist<?xml version="1.0" encoding="UTF-8"?>
<!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>
com.terryli.gmail-commander-digest.plist<?xml version="1.0" encoding="UTF-8"?>
<!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>
launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
launchctl unload ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
launchctl load ~/Library/LaunchAgents/com.terryli.gmail-commander-bot.plist
pkill -f "gmail-commander.*bot.ts"
rm -f /tmp/gmail-commander-bot.pid
# 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
| 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:
/abortcancels any in-progress compose or reply session. Works at any step in the flow.
Browser Auth (one-time, interactive)
→ Google issues: access_token (1h TTL) + refresh_token (7d TTL in Testing mode)
→ Saved to: ~/.claude/tools/gmail-tokens/<GMAIL_OP_UUID>.json
Silent Refresh (automatic, no browser)
→ Uses refresh_token to get new access_token
→ Fails with invalid_grant when refresh_token itself expires
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.
invalid_grantinvalid_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).
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
After this skill completes, reflect before closing the task:
Do NOT defer. The next invocation inherits whatever you leave behind.