Add morning-report and stock-lookup skills

Add comprehensive morning report skill with collectors for calendar, email, tasks,
infrastructure status, news, stocks, and weather. Add stock lookup skill for quote queries.
This commit is contained in:
OpenCode Test
2026-01-03 10:54:54 -08:00
parent ae958528a6
commit daa4de8832
13 changed files with 1590 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Gmail collector using existing gmail skill."""
import os
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
def fetch_unread_emails(days: int = 7, max_results: int = 15) -> list:
"""Fetch unread emails directly using gmail_mcp library."""
# Set credentials path
os.environ.setdefault('GMAIL_CREDENTIALS_PATH', os.path.expanduser('~/.gmail-mcp/credentials.json'))
try:
# Add gmail venv to path
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))
from gmail_mcp.utils.GCP.gmail_auth import get_gmail_service
service = get_gmail_service()
results = service.users().messages().list(
userId='me',
q=f'is:unread newer_than:{days}d',
maxResults=max_results
).execute()
emails = []
for msg in results.get('messages', []):
detail = service.users().messages().get(
userId='me',
id=msg['id'],
format='metadata',
metadataHeaders=['From', 'Subject']
).execute()
headers = {h['name']: h['value'] for h in detail['payload']['headers']}
emails.append({
'from': headers.get('From', 'Unknown'),
'subject': headers.get('Subject', '(no subject)'),
'id': msg['id']
})
return emails
except Exception as e:
return [{"error": str(e)}]
def triage_with_sonnet(emails: list) -> str:
"""Use Sonnet to triage and summarize emails."""
if not emails or (len(emails) == 1 and "error" in emails[0]):
error = emails[0].get("error", "Unknown error") if emails else "No data"
return f"⚠️ Could not fetch emails: {error}"
# Build email summary for Sonnet
email_text = []
for i, e in enumerate(emails[:10], 1):
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')
subject = e.get("subject", "(no subject)")[:80]
email_text.append(f"{i}. From: {sender}\n Subject: {subject}")
email_context = "\n\n".join(email_text)
prompt = f"""You are triaging emails for a morning report. Given these unread emails, provide a brief summary.
Format:
- First line: count and any urgent items (e.g., "5 unread, 1 urgent")
- Then list top emails with [!] for urgent, or plain bullet
- Keep each email to one line: sender - subject snippet (max 50 chars)
- Maximum 5 emails shown
Emails:
{email_context}
Output the formatted email section, nothing else."""
try:
result = subprocess.run(
["/home/will/.local/bin/claude", "--print", "--model", "sonnet", "-p", prompt],
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except Exception:
pass
# Fallback to basic format
lines = [f"{len(emails)} unread"]
for e in emails[:5]:
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')[:20]
subject = e.get("subject", "(no subject)")[:40]
lines.append(f"{sender} - {subject}")
return "\n".join(lines)
def collect(config: dict) -> dict:
"""Main collector entry point."""
email_config = config.get("email", {})
max_display = email_config.get("max_display", 5)
use_triage = email_config.get("triage", True)
emails = fetch_unread_emails(days=7, max_results=max_display + 10)
if use_triage and emails and "error" not in emails[0]:
formatted = triage_with_sonnet(emails)
else:
# Basic format or error
if emails and "error" not in emails[0]:
lines = [f"{len(emails)} unread"]
for e in emails[:max_display]:
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')[:20]
subject = e.get("subject", "(no subject)")[:40]
lines.append(f"{sender} - {subject}")
formatted = "\n".join(lines)
else:
error = emails[0].get("error", "Unknown") if emails else "No data"
formatted = f"⚠️ Could not fetch emails: {error}"
has_error = emails and len(emails) == 1 and "error" in emails[0]
return {
"section": "Email",
"icon": "📧",
"content": formatted,
"raw": emails if not has_error else None,
"count": len(emails) if not has_error else 0,
"error": emails[0].get("error") if has_error else None
}
if __name__ == "__main__":
config = {"email": {"max_display": 5, "triage": True}}
result = collect(config)
print(f"## {result['icon']} {result['section']}")
print(result["content"])