Fetch Year Calendar Data
Fetch full year of calendar events - raw data dump for inference analysis
Source Code
import fs from "fs";
import path from "path";
const [year, outputPath = "session/year-review.json"] = process.argv.slice(2);
const yearNum = parseInt(year, 10);
if (!yearNum || yearNum < 2000 || yearNum > 2100) {
console.error("Error: Valid year required (e.g., 2025)");
process.exit(1);
}
// Date range
const now = new Date();
const yearStart = new Date(`${yearNum}-01-01T00:00:00`);
const yearEnd = yearNum === now.getFullYear() ? now : new Date(`${yearNum}-12-31T23:59:59`);
const isPartialYear = yearNum === now.getFullYear() && now.getMonth() < 11;
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
// For period label only
const currentMonth = monthNames[now.getMonth()];
console.log(`Fetching your ${yearNum} calendar${isPartialYear ? " so far" : ""}...`);
try {
// 1. Get user info
const calRes = await fetch(
"https://www.googleapis.com/calendar/v3/calendars/primary",
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const calData = await calRes.json();
if (calData.error) {
const errMsg = calData.error.message || calData.error.reason || JSON.stringify(calData.error);
throw new Error(`Calendar API error: ${errMsg}`);
}
const userEmail = calData.id;
const userDomain = userEmail.split("@")[1];
console.log(`✓ Connected as ${userEmail}`);
// 2. Fetch ALL events for the year (paginated)
console.log(`Fetching events...`);
const allEvents = [];
let pageToken = null;
do {
const params = new URLSearchParams({
timeMin: yearStart.toISOString(),
timeMax: yearEnd.toISOString(),
singleEvents: "true",
orderBy: "startTime",
maxResults: "2500",
});
if (pageToken) params.set("pageToken", pageToken);
const eventsRes = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const eventsData = await eventsRes.json();
if (eventsData.error) {
const errMsg = eventsData.error.message || eventsData.error.reason || JSON.stringify(eventsData.error);
throw new Error(`Events fetch failed: ${errMsg}`);
}
if (eventsData.items) allEvents.push(...eventsData.items);
pageToken = eventsData.nextPageToken;
console.log(` Fetched ${allEvents.length} events...`);
} while (pageToken);
console.log(`✓ Retrieved ${allEvents.length} total calendar entries`);
// 3. Process into clean event objects - MINIMAL transformation, just structure
const events = allEvents
.filter((e) => e.status !== "cancelled") // Only filter cancelled
.map((event) => {
const startTime = event.start?.dateTime || event.start?.date;
const endTime = event.end?.dateTime || event.end?.date;
const isAllDay = !event.start?.dateTime;
// Duration in minutes
let durationMinutes = 0;
if (startTime && endTime) {
durationMinutes = Math.round((new Date(endTime) - new Date(startTime)) / 60000);
if (isAllDay) durationMinutes = 0; // All-day events don't count as time
}
// Attendees (excluding self)
const attendees = (event.attendees || [])
.filter((a) => !a.self)
.map((a) => ({
email: a.email,
name: a.displayName || a.email?.split("@")[0] || "Unknown",
response: a.responseStatus,
isExternal: !a.email?.endsWith(`@${userDomain}`),
}));
// User's own response to this event
// CRITICAL: Only count meetings user explicitly accepted
const selfAttendee = (event.attendees || []).find((a) => a.self);
const userResponse = selfAttendee?.responseStatus || (event.creator?.self ? "accepted" : "unknown");
// accepted = explicitly said yes, needsAction = never responded, tentative = maybe
const userAccepted = userResponse === "accepted";
const userDeclined = userResponse === "declined";
// Slim event object - remove derivable fields
// Inference can compute: date from start, month/day from start, end from start+duration
// attendeeCount from attendees.length, hasAttendees from attendees.length > 0
return {
title: event.summary || "(No title)",
description: event.description || null,
start: startTime,
durationMinutes,
isAllDay,
attendees,
userAccepted, // TRUE = user explicitly accepted this meeting
userDeclined, // TRUE = user explicitly declined
// If both false: user never responded (needsAction) or tentative
isRecurring: !!event.recurringEventId,
organizer: event.organizer ? {
email: event.organizer.email,
name: event.organizer.displayName || event.organizer.email?.split("@")[0],
isSelf: event.organizer.self || false,
} : null,
};
});
// 4. Basic counts ONLY - no interpretation
const totalEvents = events.length;
const acceptedEvents = events.filter((e) => e.userAccepted).length;
const declinedEvents = events.filter((e) => e.userDeclined).length;
const noResponseEvents = events.filter((e) => !e.userAccepted && !e.userDeclined).length;
// 5. Output - raw data for inference
const output = {
year: yearNum,
isPartialYear,
periodLabel: isPartialYear ? `January - ${currentMonth} ${yearNum}` : `${yearNum}`,
user: {
email: userEmail,
domain: userDomain,
},
counts: {
total: totalEvents,
accepted: acceptedEvents, // User explicitly accepted - THIS IS WHAT MATTERS
declined: declinedEvents, // User explicitly declined - exclude from analysis
noResponse: noResponseEvents, // User never responded - likely didn't attend
},
// ALL events - inference will classify and analyze
events,
};
// Write output
const dir = path.dirname(outputPath);
if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
console.log(`\n✓ Calendar data exported`);
console.log(` ${totalEvents} total events`);
console.log(` ${acceptedEvents} accepted (analyze these)`);
console.log(` ${declinedEvents} declined, ${noResponseEvents} no response (exclude these)`);
console.log(` Written to: ${outputPath}`);
} catch (err) {
console.error(`\nError: ${err.message}`);
process.exit(1);
}