Connect to Google Calendar
Fetch calendar events from the last 90 days for profile analysis
Source Code
import fs from "fs";
import path from "path";
const [outputPath = "session/calendar-raw.json"] = process.argv.slice(2);
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const now = new Date();
const formatDate = (d) =>
d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
console.log("Collecting your calendar events (last 90 days)...");
try {
// 1. Get user info and settings in parallel
console.log("Fetching calendar data...");
const [userInfo, calendars] = await Promise.all([
// Get user's primary calendar info (includes timezone)
(async () => {
const res = await fetch(
"https://www.googleapis.com/calendar/v3/calendars/primary",
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (data.error) throw new Error(`Calendar info failed: ${data.error.message}`);
return data;
})(),
// Get calendar list
(async () => {
const res = await fetch(
"https://www.googleapis.com/calendar/v3/users/me/calendarList",
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (data.error) throw new Error(`Calendar list failed: ${data.error.message}`);
return data.items || [];
})(),
]);
const userEmail = userInfo.id;
const userTimezone = userInfo.timeZone;
const primaryCalendarId = userInfo.id;
console.log(`✓ Connected as ${userEmail}`);
console.log(` Timezone: ${userTimezone}`);
console.log(` ${calendars.length} calendars found`);
// 2. Fetch events from primary calendar
console.log("Fetching events...");
const events = [];
let pageToken = null;
do {
const params = new URLSearchParams({
timeMin: ninetyDaysAgo.toISOString(),
timeMax: now.toISOString(),
singleEvents: "true", // Expand recurring events
orderBy: "startTime",
maxResults: "250",
});
if (pageToken) params.set("pageToken", pageToken);
const res = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (data.error) throw new Error(`Events fetch failed: ${data.error.message}`);
events.push(...(data.items || []));
pageToken = data.nextPageToken;
} while (pageToken);
console.log(` Found ${events.length} events`);
if (events.length === 0) {
console.error("\n✗ No events found in the last 90 days.");
console.log(
JSON.stringify({
success: false,
error: "no_events_found",
user: userEmail,
timezone: userTimezone,
})
);
process.exit(1);
}
// 3. Process events - extract relevant data for analysis
const processedEvents = events.map((event) => {
const isAllDay = !event.start?.dateTime;
const startTime = event.start?.dateTime || event.start?.date;
const endTime = event.end?.dateTime || event.end?.date;
// Calculate duration in minutes (only for timed events)
let durationMinutes = null;
if (!isAllDay && startTime && endTime) {
durationMinutes = Math.round(
(new Date(endTime) - new Date(startTime)) / (1000 * 60)
);
}
// Process attendees
const attendees = (event.attendees || []).map((a) => ({
email: a.email,
displayName: a.displayName || a.email.split("@")[0],
responseStatus: a.responseStatus, // needsAction, declined, tentative, accepted
organizer: a.organizer || false,
self: a.self || false,
}));
// Identify organizer
const organizer = event.organizer
? {
email: event.organizer.email,
displayName: event.organizer.displayName || event.organizer.email?.split("@")[0],
self: event.organizer.self || false,
}
: null;
// Check for video conferencing
const hasVideoConference = !!(
event.conferenceData ||
event.hangoutLink ||
(event.location && /zoom|meet|teams/i.test(event.location))
);
return {
id: event.id,
title: event.summary || "(No title)",
description: event.description || null,
start: startTime,
end: endTime,
isAllDay,
durationMinutes,
timezone: event.start?.timeZone || userTimezone,
location: event.location || null,
hasVideoConference,
organizer,
attendees,
attendeeCount: attendees.filter((a) => !a.self).length,
recurringEventId: event.recurringEventId || null,
isRecurring: !!event.recurringEventId,
status: event.status, // confirmed, tentative, cancelled
created: event.created,
updated: event.updated,
};
});
// Sort by start time (most recent first)
processedEvents.sort((a, b) => new Date(b.start) - new Date(a.start));
// 4. Build attendee summary for collaborator analysis
const attendeeMap = new Map();
for (const event of processedEvents) {
// Skip all-day events for collaborator scoring (often not real meetings)
if (event.isAllDay) continue;
const isOneOnOne = event.attendeeCount === 1;
for (const attendee of event.attendees) {
if (attendee.self) continue; // Skip self
if (!attendeeMap.has(attendee.email)) {
attendeeMap.set(attendee.email, {
email: attendee.email,
displayName: attendee.displayName,
oneOnOneCount: 0,
groupMeetingCount: 0,
organizedForMe: 0,
iOrganizedFor: 0,
recurringMeetings: 0,
totalMeetings: 0,
});
}
const stats = attendeeMap.get(attendee.email);
stats.totalMeetings++;
if (isOneOnOne) {
stats.oneOnOneCount++;
} else {
stats.groupMeetingCount++;
}
if (event.isRecurring) {
stats.recurringMeetings++;
}
// Track who organizes
if (event.organizer) {
if (event.organizer.self) {
stats.iOrganizedFor++;
} else if (event.organizer.email === attendee.email) {
stats.organizedForMe++;
}
}
}
}
// Convert to array and sort by engagement
const collaborators = [...attendeeMap.values()]
.map((c) => ({
...c,
// Score similar to Slack's approach
score:
c.oneOnOneCount * 5 + // 1:1s are strongest signal
c.recurringMeetings * 2 + // Recurring = consistent relationship
c.organizedForMe * 2 + // They invite me
c.iOrganizedFor * 2 + // I invite them
c.groupMeetingCount * 1, // Group meetings weakest
}))
.sort((a, b) => b.score - a.score);
// 5. Build summary statistics
const timedEvents = processedEvents.filter((e) => !e.isAllDay);
const allDayEvents = processedEvents.filter((e) => e.isAllDay);
const recurringEvents = processedEvents.filter((e) => e.isRecurring);
// Calculate events by day of week
const dayOfWeekCounts = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
for (const event of timedEvents) {
const day = new Date(event.start).getDay();
dayOfWeekCounts[day]++;
}
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const eventsByDay = Object.entries(dayOfWeekCounts)
.map(([day, count]) => ({ day: dayNames[day], count }))
.sort((a, b) => b.count - a.count);
// 6. Build output
const output = {
user: {
email: userEmail,
timezone: userTimezone,
},
period: `${formatDate(ninetyDaysAgo)} - ${formatDate(now)}`,
summary: {
totalEvents: processedEvents.length,
timedEvents: timedEvents.length,
allDayEvents: allDayEvents.length,
recurringEvents: recurringEvents.length,
uniqueCollaborators: collaborators.length,
eventsByDayOfWeek: eventsByDay,
},
events: processedEvents,
collaborators,
};
// 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✓ Collected ${processedEvents.length} events`);
console.log(` Timed meetings: ${timedEvents.length}`);
console.log(` All-day events: ${allDayEvents.length}`);
console.log(` Unique collaborators: ${collaborators.length}`);
console.log(` Output: ${outputPath}`);
console.log(
JSON.stringify({
success: true,
outputPath,
user: userEmail,
timezone: userTimezone,
eventCount: processedEvents.length,
collaboratorCount: collaborators.length,
})
);
} catch (error) {
console.error("Failed:", error.message);
throw error;
}