import { ImapFlow } from "imapflow"; import { simpleParser } from "mailparser"; import { ClaudeBridge } from "../../channels/slack/bridge"; import { sendEmail } from "../../channels/email/send"; import * as fs from "fs"; import * as path from "path"; const pass = process.env.PURELYMAIL_PASS; if (!pass) { console.error("Missing PURELYMAIL_PASS"); process.exit(1); } const TASKS_SYNC_DIR = path.resolve(__dirname, "../../../../palaces/manglasabang/secretariat/tasks-synced-from-linear-to-git"); const WMO_CODES: Record = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 80: "Slight showers", 81: "Moderate showers", 82: "Violent showers", 95: "Thunderstorm", 96: "Thunderstorm w/ hail", 99: "Thunderstorm w/ heavy hail", }; function wmoLabel(code: number): string { return WMO_CODES[code] ?? `Weather code ${code}`; } async function fetchWeather(): Promise { const url = "https://api.open-meteo.com/v1/forecast" + "?latitude=37.323&longitude=-122.032" + "¤t=temperature_2m,apparent_temperature,precipitation,weathercode,windspeed_10m" + "&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_sum" + "&timezone=America%2FLos_Angeles&forecast_days=3" + "&temperature_unit=celsius&wind_speed_unit=kmh&precipitation_unit=mm"; const res = await fetch(url); const data = (await res.json()) as any; const c = data.current; const d = data.daily; return [ `Now: ${c.temperature_2m}°C (feels ${c.apparent_temperature}°C) — ${wmoLabel(c.weathercode)}, wind ${c.windspeed_10m} km/h`, `Today: ${d.temperature_2m_max[0]}°C / ${d.temperature_2m_min[0]}°C — ${wmoLabel(d.weathercode[0])}, precip ${d.precipitation_sum[0]} mm`, `Tomorrow: ${d.temperature_2m_max[1]}°C / ${d.temperature_2m_min[1]}°C — ${wmoLabel(d.weathercode[1])}, precip ${d.precipitation_sum[1]} mm`, `Day after: ${d.temperature_2m_max[2]}°C / ${d.temperature_2m_min[2]}°C — ${wmoLabel(d.weathercode[2])}`, ].join("\n"); } function readActiveTasks(): string { const chunks: string[] = []; if (!fs.existsSync(TASKS_SYNC_DIR)) return "(no active tasks)"; const teams = fs.readdirSync(TASKS_SYNC_DIR, { withFileTypes: true }); for (const team of teams) { if (!team.isDirectory()) continue; const activeDir = path.join(TASKS_SYNC_DIR, team.name, "active"); if (!fs.existsSync(activeDir)) continue; const tasks = fs.readdirSync(activeDir, { withFileTypes: true }); for (const task of tasks) { if (!task.isDirectory()) continue; const mdPath = path.join(activeDir, task.name, `${task.name}.md`); if (fs.existsSync(mdPath)) { const content = fs.readFileSync(mdPath, "utf8").trim(); chunks.push(`=== ${task.name} ===\n${content}`); } } } return chunks.length > 0 ? chunks.join("\n\n") : "(no active tasks)"; } async function fetchRecentEmails(): Promise { const ACCOUNTS = ["ace@manglasabang.com", "ace@palace.fund"]; const SELF = new Set(ACCOUNTS.map((a) => a.toLowerCase())); const allLines: string[] = []; for (const account of ACCOUNTS) { const client = new ImapFlow({ host: "mailserver.purelymail.com", port: 993, secure: true, auth: { user: account, pass: pass! }, logger: false, }); try { await client.connect(); const lock = await client.getMailboxLock("INBOX"); try { const since = new Date(Date.now() - 24 * 60 * 60 * 1000); const uids = (await client.search({ since }, { uid: true })) || []; const recent = uids.slice(-30); for (const uid of recent) { const msg = await client.fetchOne(String(uid), { source: true }, { uid: true }); if (!msg?.source) continue; const mail = await simpleParser(msg.source); const from = mail.from?.value?.[0]?.address?.toLowerCase() || ""; if (SELF.has(from)) continue; const subject = mail.subject || "(no subject)"; const snippet = (mail.text || "").trim().slice(0, 300).replace(/\n+/g, " "); allLines.push(`[${account}] From: ${from} | Subject: ${subject}\n${snippet}`); } } finally { lock.release(); } await client.logout(); } catch (err) { console.error(`[briefing] email check failed for ${account}:`, err); } } return allLines.length > 0 ? allLines.join("\n\n---\n\n") : "(no incoming emails in last 24h)"; } async function main() { console.log("[briefing] starting..."); const launchTime = new Date(); const todayPST = launchTime.toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" }); const hourPST = Number(launchTime.toLocaleString("en-US", { hour: "numeric", hour12: false, timeZone: "America/Los_Angeles" })); const lockFile = path.resolve(__dirname, ".last-sent"); if (fs.existsSync(lockFile) && fs.readFileSync(lockFile, "utf8").trim() === todayPST) { console.log(`[briefing] already sent today (${todayPST}), skipping`); process.exit(0); } if (hourPST < 4 || hourPST > 9) { console.log(`[briefing] outside send window (hour=${hourPST}), skipping stale catch-up`); process.exit(0); } const [weather, recentEmails] = await Promise.all([ fetchWeather().catch((err) => { console.error("[briefing] weather failed:", err); return "(weather unavailable)"; }), fetchRecentEmails(), ]); const taskContent = readActiveTasks(); const now = new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "America/Los_Angeles", }); const prompt = `You are Ace, Junwon's personal AI assistant. It is ${now}. Write Junwon's daily morning briefing email in Markdown. Guidelines: - Be direct, brief, and useful. No filler, no pleasantries beyond a single line. - Organize with clear ## headers - Flag anything that needs Junwon's action today (use **bold** or a ⚠️) - Keep the whole thing scannable — he reads this on his phone at 6AM Sections to include (only include emails section if there's something worth mentioning): 1. Brief date/day line 2. ## Weather — Cupertino 3. ## Active Tasks — what's in flight, what's blocked on Junwon 4. ## Emails — anything worth flagging (skip if nothing notable) 5. ## Today's Priorities — short bulleted list of what he should focus on --- WEATHER DATA: ${weather} ACTIVE TASKS: ${taskContent} RECENT EMAILS (last 24h): ${recentEmails}`; console.log("[briefing] calling Claude..."); const bridge = new ClaudeBridge(); bridge.channel = "briefing"; bridge.model = "sonnet"; const briefingMd = await bridge.send(prompt); bridge.kill(); await sendEmail("ace@manglasabang.com", { to: "junwon@manglasabang.com", subject: `Daily Briefing — ${now}`, markdown: briefingMd, }); fs.writeFileSync(lockFile, todayPST); console.log(`[briefing] sent to junwon@manglasabang.com`); } main().catch((err) => { console.error("[briefing] fatal:", err); process.exit(1); });