- Add is_section_enabled() to support per-section enable/disable in config - Update Python path from 3.13 to 3.14 for gmail venv - Disable tasks section by default (enabled: false in config) - Apply code formatting improvements (black/ruff style) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
182 lines
5.1 KiB
Python
Executable File
182 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Google Tasks collector."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Add gmail venv to path for Google API libraries
|
|
venv_site = Path.home() / ".claude/mcp/gmail/venv/lib/python3.14/site-packages"
|
|
if str(venv_site) not in sys.path:
|
|
sys.path.insert(0, str(venv_site))
|
|
|
|
# Google Tasks API
|
|
try:
|
|
from google.oauth2.credentials import Credentials
|
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
from google.auth.transport.requests import Request
|
|
from googleapiclient.discovery import build
|
|
|
|
GOOGLE_API_AVAILABLE = True
|
|
except ImportError:
|
|
GOOGLE_API_AVAILABLE = False
|
|
|
|
|
|
SCOPES = ["https://www.googleapis.com/auth/tasks.readonly"]
|
|
TOKEN_PATH = Path.home() / ".gmail-mcp/tasks_token.json"
|
|
CREDS_PATH = Path.home() / ".gmail-mcp/credentials.json"
|
|
|
|
|
|
def get_credentials():
|
|
"""Get or refresh Google credentials for Tasks API."""
|
|
creds = None
|
|
|
|
if TOKEN_PATH.exists():
|
|
creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), SCOPES)
|
|
|
|
if not creds or not creds.valid:
|
|
if creds and creds.expired and creds.refresh_token:
|
|
creds.refresh(Request())
|
|
else:
|
|
if not CREDS_PATH.exists():
|
|
return None
|
|
flow = InstalledAppFlow.from_client_secrets_file(str(CREDS_PATH), SCOPES)
|
|
creds = flow.run_local_server(port=0)
|
|
|
|
TOKEN_PATH.write_text(creds.to_json())
|
|
|
|
return creds
|
|
|
|
|
|
def fetch_tasks(max_results: int = 10) -> list:
|
|
"""Fetch tasks from Google Tasks API."""
|
|
if not GOOGLE_API_AVAILABLE:
|
|
return [{"error": "Google API libraries not installed"}]
|
|
|
|
try:
|
|
creds = get_credentials()
|
|
if not creds:
|
|
return [
|
|
{
|
|
"error": "Tasks API not authenticated - run: ~/.claude/mcp/gmail/venv/bin/python ~/.claude/skills/morning-report/scripts/collectors/gtasks.py --auth"
|
|
}
|
|
]
|
|
|
|
service = build("tasks", "v1", credentials=creds)
|
|
|
|
# Get default task list
|
|
tasklists = service.tasklists().list(maxResults=1).execute()
|
|
if not tasklists.get("items"):
|
|
return []
|
|
|
|
tasklist_id = tasklists["items"][0]["id"]
|
|
|
|
# Get tasks
|
|
results = (
|
|
service.tasks()
|
|
.list(
|
|
tasklist=tasklist_id,
|
|
maxResults=max_results,
|
|
showCompleted=False,
|
|
showHidden=False,
|
|
)
|
|
.execute()
|
|
)
|
|
|
|
tasks = results.get("items", [])
|
|
return tasks
|
|
|
|
except Exception as e:
|
|
return [{"error": str(e)}]
|
|
|
|
|
|
def format_tasks(tasks: list, max_display: int = 5) -> str:
|
|
"""Format tasks - no LLM needed, structured data."""
|
|
if not tasks:
|
|
return "No pending tasks"
|
|
|
|
if len(tasks) == 1 and "error" in tasks[0]:
|
|
return f"⚠️ Could not fetch tasks: {tasks[0]['error']}"
|
|
|
|
lines = []
|
|
|
|
# Count and header
|
|
total = len(tasks)
|
|
due_today = 0
|
|
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
for task in tasks:
|
|
due = task.get("due", "")
|
|
if due and due.startswith(today_str):
|
|
due_today += 1
|
|
|
|
header = f"{total} pending"
|
|
if due_today > 0:
|
|
header += f", {due_today} due today"
|
|
lines.append(header)
|
|
|
|
# List tasks
|
|
for task in tasks[:max_display]:
|
|
title = task.get("title", "(No title)")
|
|
due = task.get("due", "")
|
|
|
|
due_str = ""
|
|
if due:
|
|
try:
|
|
due_date = datetime.fromisoformat(due.replace("Z", "+00:00"))
|
|
if due_date.date() == datetime.now().date():
|
|
due_str = " (due today)"
|
|
elif due_date.date() < datetime.now().date():
|
|
due_str = " (overdue!)"
|
|
else:
|
|
due_str = f" (due {due_date.strftime('%b %d')})"
|
|
except ValueError:
|
|
pass
|
|
|
|
lines.append(f" • {title}{due_str}")
|
|
|
|
if total > max_display:
|
|
lines.append(f" ... and {total - max_display} more")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def collect(config: dict) -> dict:
|
|
"""Main collector entry point."""
|
|
tasks_config = config.get("tasks", {})
|
|
max_display = tasks_config.get("max_display", 5)
|
|
|
|
tasks = fetch_tasks(max_display + 5)
|
|
formatted = format_tasks(tasks, max_display)
|
|
|
|
has_error = tasks and len(tasks) == 1 and "error" in tasks[0]
|
|
|
|
return {
|
|
"section": "Tasks",
|
|
"icon": "✅",
|
|
"content": formatted,
|
|
"raw": tasks if not has_error else None,
|
|
"count": len(tasks) if not has_error else 0,
|
|
"error": tasks[0].get("error") if has_error else None,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
if "--auth" in sys.argv:
|
|
print("Starting Tasks API authentication...")
|
|
creds = get_credentials()
|
|
if creds:
|
|
print(f"✅ Authentication successful! Token saved to {TOKEN_PATH}")
|
|
else:
|
|
print("❌ Authentication failed")
|
|
sys.exit(0)
|
|
|
|
config = {"tasks": {"max_display": 5}}
|
|
result = collect(config)
|
|
print(f"## {result['icon']} {result['section']}")
|
|
print(result["content"])
|