Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3cb082c36 | |||
| db0d9f97b2 | |||
| 94603b19a5 | |||
| 45b7e4bcf7 |
@@ -4,10 +4,10 @@
|
||||
"frontend-design@claude-plugins-official": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/frontend-design/6d3752c000e2",
|
||||
"version": "6d3752c000e2",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/frontend-design/15b07b46dab3",
|
||||
"version": "15b07b46dab3",
|
||||
"installedAt": "2025-12-24T19:08:12.422Z",
|
||||
"lastUpdated": "2025-12-24T19:08:12.422Z",
|
||||
"lastUpdated": "2026-01-05T07:21:36.978Z",
|
||||
"gitCommitSha": "6d3752c000e2b3d0e6137bd7adb04895d6f40f14",
|
||||
"isLocal": true
|
||||
}
|
||||
@@ -26,10 +26,10 @@
|
||||
"commit-commands@claude-plugins-official": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/commit-commands/6d3752c000e2",
|
||||
"version": "6d3752c000e2",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/commit-commands/15b07b46dab3",
|
||||
"version": "15b07b46dab3",
|
||||
"installedAt": "2025-12-24T19:10:05.451Z",
|
||||
"lastUpdated": "2025-12-24T19:10:36.843Z",
|
||||
"lastUpdated": "2026-01-05T07:21:36.984Z",
|
||||
"isLocal": true
|
||||
}
|
||||
],
|
||||
@@ -69,10 +69,10 @@
|
||||
"ralph-wiggum@claude-plugins-official": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/ralph-wiggum/6d3752c000e2",
|
||||
"version": "6d3752c000e2",
|
||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/ralph-wiggum/15b07b46dab3",
|
||||
"version": "15b07b46dab3",
|
||||
"installedAt": "2026-01-02T19:47:02.395Z",
|
||||
"lastUpdated": "2026-01-02T19:47:11.472Z",
|
||||
"lastUpdated": "2026-01-05T07:21:36.997Z",
|
||||
"gitCommitSha": "de89f3066c68d7a2f2d4190173fa46c26e2f30fd",
|
||||
"isLocal": true
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"repo": "anthropics/claude-plugins-official"
|
||||
},
|
||||
"installLocation": "/home/will/.claude/plugins/marketplaces/claude-plugins-official",
|
||||
"lastUpdated": "2026-01-04T20:00:29.464Z"
|
||||
"lastUpdated": "2026-01-05T07:22:46.460Z"
|
||||
},
|
||||
"superpowers-marketplace": {
|
||||
"source": {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# Morning Report - Sun Jan 04, 2026
|
||||
|
||||
## 🌤 Weather
|
||||
Weather unavailable: <urlopen error timed out>
|
||||
Seattle: 51°F, Partly cloudy | High 52° Low 43°
|
||||
|
||||
## 📧 Email
|
||||
10 unread, 0 urgent
|
||||
|
||||
- Chase - Your Chase Freedom Unlimited Visa balance is...
|
||||
- Chase - Your rewards balance has reached 0 POINTS
|
||||
- USPS - PO Box Price Changes Coming
|
||||
- Uber Receipts - Your Saturday evening trip with Uber
|
||||
- Chase - You made an online, phone, or mail transaction...
|
||||
15 unread
|
||||
• Capital One | Quicks - Your requested balance summary
|
||||
• Uber Receipts - [Personal] Your Saturday evening trip wi
|
||||
• Experian - William, it's time to check your utiliza
|
||||
• Experteer Search Age - William, we have 2 new opportunities for
|
||||
• Chase - You can start your mortgage preapproval
|
||||
|
||||
## 📅 Today
|
||||
• 2:00 PM - Seattle Saturday (SAM + QED + Lecosho) (5h)
|
||||
@@ -18,17 +17,14 @@ Weather unavailable: <urlopen error timed out>
|
||||
## 📈 Stocks
|
||||
CRWV $79.32 +10.8% ▲ NVDA $188.85 +1.3% ▲ MSFT $472.94 -2.2% ▼
|
||||
|
||||
## ✅ Tasks
|
||||
⚠️ Could not fetch tasks: ('invalid_scope: Bad Request', {'error': 'invalid_scope', 'error_description': 'Bad Request'})
|
||||
|
||||
## 🖥 Infrastructure
|
||||
K8s: 🟢 | Workstation: 🟢
|
||||
|
||||
## 📰 Tech News
|
||||
• Anti-Aging Injection Regrows Knee Cartilage and Prevents Art... (Hacker News)
|
||||
• How I archived 10 years of memories using Spotify (Hacker News)
|
||||
• C-Sentinel: System prober that captures "system fingerprints... (Hacker News)
|
||||
• Show HN: An LLM-Powered PCB Schematic Checker (Major Update) (Hacker News)
|
||||
• Can I finally start using Wayland in 2026? (Lobsters)
|
||||
• Saying goodbye to the servers at our physical datacenter - S... (Lobsters)
|
||||
• Saying goodbye to the servers at our physical datacenter (Lobsters)
|
||||
|
||||
---
|
||||
*Generated: 2026-01-04 08:00:33 PT*
|
||||
*Generated: 2026-01-04 14:40:48 PT*
|
||||
+11
-15
@@ -1,16 +1,15 @@
|
||||
# Morning Report - Sun Jan 04, 2026
|
||||
|
||||
## 🌤 Weather
|
||||
Weather unavailable: <urlopen error timed out>
|
||||
Seattle: 51°F, Partly cloudy | High 52° Low 43°
|
||||
|
||||
## 📧 Email
|
||||
10 unread, 0 urgent
|
||||
|
||||
- Chase - Your Chase Freedom Unlimited Visa balance is...
|
||||
- Chase - Your rewards balance has reached 0 POINTS
|
||||
- USPS - PO Box Price Changes Coming
|
||||
- Uber Receipts - Your Saturday evening trip with Uber
|
||||
- Chase - You made an online, phone, or mail transaction...
|
||||
15 unread
|
||||
• Capital One | Quicks - Your requested balance summary
|
||||
• Uber Receipts - [Personal] Your Saturday evening trip wi
|
||||
• Experian - William, it's time to check your utiliza
|
||||
• Experteer Search Age - William, we have 2 new opportunities for
|
||||
• Chase - You can start your mortgage preapproval
|
||||
|
||||
## 📅 Today
|
||||
• 2:00 PM - Seattle Saturday (SAM + QED + Lecosho) (5h)
|
||||
@@ -18,17 +17,14 @@ Weather unavailable: <urlopen error timed out>
|
||||
## 📈 Stocks
|
||||
CRWV $79.32 +10.8% ▲ NVDA $188.85 +1.3% ▲ MSFT $472.94 -2.2% ▼
|
||||
|
||||
## ✅ Tasks
|
||||
⚠️ Could not fetch tasks: ('invalid_scope: Bad Request', {'error': 'invalid_scope', 'error_description': 'Bad Request'})
|
||||
|
||||
## 🖥 Infrastructure
|
||||
K8s: 🟢 | Workstation: 🟢
|
||||
|
||||
## 📰 Tech News
|
||||
• Anti-Aging Injection Regrows Knee Cartilage and Prevents Art... (Hacker News)
|
||||
• How I archived 10 years of memories using Spotify (Hacker News)
|
||||
• C-Sentinel: System prober that captures "system fingerprints... (Hacker News)
|
||||
• Show HN: An LLM-Powered PCB Schematic Checker (Major Update) (Hacker News)
|
||||
• Can I finally start using Wayland in 2026? (Lobsters)
|
||||
• Saying goodbye to the servers at our physical datacenter - S... (Lobsters)
|
||||
• Saying goodbye to the servers at our physical datacenter (Lobsters)
|
||||
|
||||
---
|
||||
*Generated: 2026-01-04 08:00:33 PT*
|
||||
*Generated: 2026-01-04 14:40:48 PT*
|
||||
@@ -25,6 +25,7 @@
|
||||
"show_tomorrow": true
|
||||
},
|
||||
"tasks": {
|
||||
"enabled": false,
|
||||
"max_display": 5,
|
||||
"show_due_dates": true
|
||||
},
|
||||
|
||||
@@ -9,11 +9,13 @@ from pathlib import Path
|
||||
|
||||
def fetch_events(mode: str = "today") -> list:
|
||||
"""Fetch calendar events directly using gmail_mcp library."""
|
||||
os.environ.setdefault('GMAIL_CREDENTIALS_PATH', os.path.expanduser('~/.gmail-mcp/credentials.json'))
|
||||
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"
|
||||
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))
|
||||
|
||||
@@ -22,26 +24,32 @@ def fetch_events(mode: str = "today") -> list:
|
||||
service = get_calendar_service()
|
||||
now = datetime.utcnow()
|
||||
|
||||
if mode == 'today':
|
||||
if mode == "today":
|
||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end = start + timedelta(days=1)
|
||||
elif mode == 'tomorrow':
|
||||
start = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif mode == "tomorrow":
|
||||
start = (now + timedelta(days=1)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
end = start + timedelta(days=1)
|
||||
else:
|
||||
start = now
|
||||
end = now + timedelta(days=7)
|
||||
|
||||
events_result = service.events().list(
|
||||
calendarId='primary',
|
||||
timeMin=start.isoformat() + 'Z',
|
||||
timeMax=end.isoformat() + 'Z',
|
||||
singleEvents=True,
|
||||
orderBy='startTime',
|
||||
maxResults=20
|
||||
).execute()
|
||||
events_result = (
|
||||
service.events()
|
||||
.list(
|
||||
calendarId="primary",
|
||||
timeMin=start.isoformat() + "Z",
|
||||
timeMax=end.isoformat() + "Z",
|
||||
singleEvents=True,
|
||||
orderBy="startTime",
|
||||
maxResults=20,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return events_result.get('items', [])
|
||||
return events_result.get("items", [])
|
||||
|
||||
except Exception as e:
|
||||
return [{"error": str(e)}]
|
||||
@@ -62,7 +70,9 @@ def format_events(today_events: list, tomorrow_events: list = None) -> str:
|
||||
|
||||
if "dateTime" in start:
|
||||
# Timed event
|
||||
dt = datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00"))
|
||||
dt = datetime.fromisoformat(
|
||||
start["dateTime"].replace("Z", "+00:00")
|
||||
)
|
||||
time_str = dt.strftime("%I:%M %p").lstrip("0")
|
||||
elif "date" in start:
|
||||
time_str = "All day"
|
||||
@@ -73,13 +83,19 @@ def format_events(today_events: list, tomorrow_events: list = None) -> str:
|
||||
# Calculate duration if end time available
|
||||
end = event.get("end", {})
|
||||
if "dateTime" in start and "dateTime" in end:
|
||||
start_dt = datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00"))
|
||||
end_dt = datetime.fromisoformat(end["dateTime"].replace("Z", "+00:00"))
|
||||
start_dt = datetime.fromisoformat(
|
||||
start["dateTime"].replace("Z", "+00:00")
|
||||
)
|
||||
end_dt = datetime.fromisoformat(
|
||||
end["dateTime"].replace("Z", "+00:00")
|
||||
)
|
||||
mins = int((end_dt - start_dt).total_seconds() / 60)
|
||||
if mins >= 60:
|
||||
hours = mins // 60
|
||||
remaining = mins % 60
|
||||
duration = f" ({hours}h{remaining}m)" if remaining else f" ({hours}h)"
|
||||
duration = (
|
||||
f" ({hours}h{remaining}m)" if remaining else f" ({hours}h)"
|
||||
)
|
||||
else:
|
||||
duration = f" ({mins}m)"
|
||||
|
||||
@@ -92,17 +108,23 @@ def format_events(today_events: list, tomorrow_events: list = None) -> str:
|
||||
|
||||
# Tomorrow preview
|
||||
if tomorrow_events is not None:
|
||||
if tomorrow_events and (len(tomorrow_events) == 0 or "error" not in tomorrow_events[0]):
|
||||
if tomorrow_events and (
|
||||
len(tomorrow_events) == 0 or "error" not in tomorrow_events[0]
|
||||
):
|
||||
count = len(tomorrow_events)
|
||||
if count > 0:
|
||||
first = tomorrow_events[0]
|
||||
start = first.get("start", {})
|
||||
if "dateTime" in start:
|
||||
dt = datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00"))
|
||||
dt = datetime.fromisoformat(
|
||||
start["dateTime"].replace("Z", "+00:00")
|
||||
)
|
||||
first_time = dt.strftime("%I:%M %p").lstrip("0")
|
||||
else:
|
||||
first_time = "All day"
|
||||
lines.append(f"Tomorrow: {count} event{'s' if count > 1 else ''}, first at {first_time}")
|
||||
lines.append(
|
||||
f"Tomorrow: {count} event{'s' if count > 1 else ''}, first at {first_time}"
|
||||
)
|
||||
else:
|
||||
lines.append("Tomorrow: No events")
|
||||
|
||||
@@ -126,7 +148,7 @@ def collect(config: dict) -> dict:
|
||||
"icon": "📅",
|
||||
"content": formatted,
|
||||
"raw": {"today": today_events, "tomorrow": tomorrow_events},
|
||||
"error": today_events[0].get("error") if has_error else None
|
||||
"error": today_events[0].get("error") if has_error else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,37 +11,49 @@ 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'))
|
||||
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"
|
||||
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))
|
||||
|
||||
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()
|
||||
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']
|
||||
})
|
||||
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
|
||||
|
||||
@@ -79,10 +91,17 @@ Output the formatted email section, nothing else."""
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["/home/will/.local/bin/claude", "--print", "--model", "sonnet", "-p", prompt],
|
||||
[
|
||||
"/home/will/.local/bin/claude",
|
||||
"--print",
|
||||
"--model",
|
||||
"sonnet",
|
||||
"-p",
|
||||
prompt,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
@@ -131,7 +150,7 @@ def collect(config: dict) -> dict:
|
||||
"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
|
||||
"error": emails[0].get("error") if has_error else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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"
|
||||
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))
|
||||
|
||||
@@ -18,6 +18,7 @@ try:
|
||||
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
|
||||
@@ -57,7 +58,11 @@ def fetch_tasks(max_results: int = 10) -> list:
|
||||
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"}]
|
||||
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)
|
||||
|
||||
@@ -69,12 +74,16 @@ def fetch_tasks(max_results: int = 10) -> list:
|
||||
tasklist_id = tasklists["items"][0]["id"]
|
||||
|
||||
# Get tasks
|
||||
results = service.tasks().list(
|
||||
tasklist=tasklist_id,
|
||||
maxResults=max_results,
|
||||
showCompleted=False,
|
||||
showHidden=False
|
||||
).execute()
|
||||
results = (
|
||||
service.tasks()
|
||||
.list(
|
||||
tasklist=tasklist_id,
|
||||
maxResults=max_results,
|
||||
showCompleted=False,
|
||||
showHidden=False,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
tasks = results.get("items", [])
|
||||
return tasks
|
||||
@@ -150,7 +159,7 @@ def collect(config: dict) -> dict:
|
||||
"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
|
||||
"error": tasks[0].get("error") if has_error else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from collectors import weather, stocks, infra, news
|
||||
# These may fail if gmail venv not activated
|
||||
try:
|
||||
from collectors import gmail, gcal, gtasks
|
||||
|
||||
GOOGLE_COLLECTORS = True
|
||||
except ImportError:
|
||||
GOOGLE_COLLECTORS = False
|
||||
@@ -29,10 +30,7 @@ LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_PATH),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
handlers=[logging.FileHandler(LOG_PATH), logging.StreamHandler()],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,10 +56,17 @@ def collect_section(name: str, collector_func, config: dict) -> dict:
|
||||
"section": name,
|
||||
"icon": "❓",
|
||||
"content": f"⚠️ {name} unavailable: {e}",
|
||||
"error": str(e)
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
def is_section_enabled(name: str, config: dict) -> bool:
|
||||
"""Check if a section is enabled in config."""
|
||||
section_key = name.lower()
|
||||
section_config = config.get(section_key, {})
|
||||
return section_config.get("enabled", True)
|
||||
|
||||
|
||||
def collect_all(config: dict) -> list:
|
||||
"""Collect all sections in parallel."""
|
||||
collectors = [
|
||||
@@ -72,11 +77,12 @@ def collect_all(config: dict) -> list:
|
||||
]
|
||||
|
||||
if GOOGLE_COLLECTORS:
|
||||
collectors.extend([
|
||||
("Email", gmail.collect),
|
||||
("Calendar", gcal.collect),
|
||||
("Tasks", gtasks.collect),
|
||||
])
|
||||
if is_section_enabled("email", config):
|
||||
collectors.append(("Email", gmail.collect))
|
||||
if is_section_enabled("calendar", config):
|
||||
collectors.append(("Calendar", gcal.collect))
|
||||
if is_section_enabled("tasks", config):
|
||||
collectors.append(("Tasks", gtasks.collect))
|
||||
else:
|
||||
logger.warning("Google collectors not available - run with gmail venv")
|
||||
|
||||
@@ -95,12 +101,14 @@ def collect_all(config: dict) -> list:
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Future {name} exception: {e}")
|
||||
results.append({
|
||||
"section": name,
|
||||
"icon": "❓",
|
||||
"content": f"⚠️ {name} failed: {e}",
|
||||
"error": str(e)
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"section": name,
|
||||
"icon": "❓",
|
||||
"content": f"⚠️ {name} failed: {e}",
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@@ -111,13 +119,21 @@ def render_report(sections: list, config: dict) -> str:
|
||||
date_str = now.strftime("%a %b %d, %Y")
|
||||
time_str = now.strftime("%I:%M %p %Z").strip()
|
||||
|
||||
lines = [
|
||||
f"# Morning Report - {date_str}",
|
||||
""
|
||||
]
|
||||
lines = [f"# Morning Report - {date_str}", ""]
|
||||
|
||||
# Order sections
|
||||
order = ["Weather", "Email", "Calendar", "Today", "Stocks", "Tasks", "Infra", "Infrastructure", "News", "Tech News"]
|
||||
order = [
|
||||
"Weather",
|
||||
"Email",
|
||||
"Calendar",
|
||||
"Today",
|
||||
"Stocks",
|
||||
"Tasks",
|
||||
"Infra",
|
||||
"Infrastructure",
|
||||
"News",
|
||||
"Tech News",
|
||||
]
|
||||
|
||||
# Sort by order
|
||||
section_map = {s.get("section", ""): s for s in sections}
|
||||
@@ -137,10 +153,7 @@ def render_report(sections: list, config: dict) -> str:
|
||||
lines.append("")
|
||||
|
||||
# Footer
|
||||
lines.extend([
|
||||
"---",
|
||||
f"*Generated: {now.strftime('%Y-%m-%d %H:%M:%S')} PT*"
|
||||
])
|
||||
lines.extend(["---", f"*Generated: {now.strftime('%Y-%m-%d %H:%M:%S')} PT*"])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -148,7 +161,9 @@ def render_report(sections: list, config: dict) -> str:
|
||||
def save_report(content: str, config: dict) -> Path:
|
||||
"""Save report to file and archive."""
|
||||
output_config = config.get("output", {})
|
||||
output_path = Path(output_config.get("path", "~/.claude/reports/morning.md")).expanduser()
|
||||
output_path = Path(
|
||||
output_config.get("path", "~/.claude/reports/morning.md")
|
||||
).expanduser()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write main report
|
||||
|
||||
@@ -189,6 +189,41 @@
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
},
|
||||
{
|
||||
"id": "2026-01-04_13-30-26",
|
||||
"started": "2026-01-04T13:30:26-08:00",
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
},
|
||||
{
|
||||
"id": "2026-01-04_14-20-18",
|
||||
"started": "2026-01-04T14:20:18-08:00",
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
},
|
||||
{
|
||||
"id": "2026-01-04_23-21-33",
|
||||
"started": "2026-01-04T23:21:33-08:00",
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
},
|
||||
{
|
||||
"id": "2026-01-04_23-22-43",
|
||||
"started": "2026-01-04T23:22:43-08:00",
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
},
|
||||
{
|
||||
"id": "2026-01-04_23-22-43",
|
||||
"started": "2026-01-04T23:22:43-08:00",
|
||||
"ended": null,
|
||||
"summarized": false,
|
||||
"topics": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user