code icon Code

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);
}