Add comprehensive morning report skill with collectors for calendar, email, tasks, infrastructure status, news, stocks, and weather. Add stock lookup skill for quote queries.
173 lines
5.0 KiB
Python
Executable File
173 lines
5.0 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.13/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"])
|